mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233259be40 | ||
|
|
107def9ccc | ||
|
|
d46ce783c2 | ||
|
|
d743472b16 | ||
|
|
d785b263f4 | ||
|
|
6d37a4e333 | ||
|
|
eb49e51ffb | ||
|
|
734d4bfc0c | ||
|
|
0e75bbda58 | ||
|
|
8b5eb21ac3 | ||
|
|
c958e4c34e | ||
|
|
ab48d830d1 | ||
|
|
915579c0a4 | ||
|
|
0a6e6f02a4 | ||
|
|
a135f45844 | ||
|
|
b39b91774d | ||
|
|
2275248813 | ||
|
|
75f4a91f11 | ||
|
|
d0b5c6c854 | ||
|
|
bd8940ae0a | ||
|
|
1be64f0d11 | ||
|
|
1c8f4b3a35 | ||
|
|
3e5176a79c | ||
|
|
53c0f2a9df | ||
|
|
28389aa38c | ||
|
|
fb5c825cf6 | ||
|
|
a0e0da1da9 | ||
|
|
a826859202 | ||
|
|
71b4a1de94 | ||
|
|
93a2e4b1ca | ||
|
|
436266b1a4 | ||
|
|
19427e33a0 | ||
|
|
48d683be77 | ||
|
|
e1ec264345 | ||
|
|
9aad2efdb0 | ||
|
|
f6595a02c4 | ||
|
|
92ca9bf7eb | ||
|
|
05242a83f0 | ||
|
|
add44c0f18 | ||
|
|
a1ebf3f90e | ||
|
|
626d50d3e9 | ||
|
|
eaa7ad85f8 | ||
|
|
be9be0b2d1 | ||
|
|
f196999c67 | ||
|
|
e674ec4f33 | ||
|
|
f23805550f | ||
|
|
55a356bbec | ||
|
|
954aaeb06b | ||
|
|
27e272c58f | ||
|
|
3ed9912bcb | ||
|
|
e0f6563a39 | ||
|
|
caa64724d0 | ||
|
|
a2011b8bc7 | ||
|
|
40fd9e935a | ||
|
|
b88c143914 | ||
|
|
0a05007c4e | ||
|
|
c47ba3f804 | ||
|
|
79a3f361a7 | ||
|
|
65399a37e5 | ||
|
|
25a3fcf731 | ||
|
|
17e2c2df8e | ||
|
|
240345d527 | ||
|
|
2d284d9277 | ||
|
|
bfa5c5944f | ||
|
|
e1051cd40f | ||
|
|
13029cc7fe | ||
|
|
58d9e4e194 | ||
|
|
4de7036e2f |
19
.circleci/config.yml
Normal file
19
.circleci/config.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
working_directory: ~/build
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:latest
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker:
|
||||||
|
version: 17.05.0-ce
|
||||||
|
- run: make image
|
||||||
|
- deploy:
|
||||||
|
command: |
|
||||||
|
if [[ "$CIRCLE_BRANCH" == "master" ]]; then
|
||||||
|
docker tag ctop quay.io/vektorlab/ctop:latest
|
||||||
|
docker tag ctop quay.io/vektorlab/ctop:$(cat VERSION)
|
||||||
|
docker login -u $DOCKER_USER -p $DOCKER_PASS quay.io
|
||||||
|
docker push quay.io/vektorlab/ctop
|
||||||
|
fi
|
||||||
15
Dockerfile
15
Dockerfile
@@ -1,3 +1,16 @@
|
|||||||
|
FROM quay.io/vektorcloud/go:1.9
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
COPY . /go/src/github.com/bcicen/ctop
|
||||||
|
RUN make build && \
|
||||||
|
mkdir -p /go/bin && \
|
||||||
|
mv -v ctop /go/bin/
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY ./ctop /ctop
|
COPY --from=0 /go/bin/ctop /ctop
|
||||||
ENTRYPOINT ["/ctop"]
|
ENTRYPOINT ["/ctop"]
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
FROM quay.io/vektorcloud/go:1.8
|
|
||||||
|
|
||||||
RUN apk add --no-cache make
|
|
||||||
|
|
||||||
COPY glide.* /go/src/github.com/bcicen/ctop/
|
|
||||||
WORKDIR /go/src/github.com/bcicen/ctop/
|
|
||||||
RUN glide install
|
|
||||||
|
|
||||||
COPY . /go/src/github.com/bcicen/ctop
|
|
||||||
RUN make build && \
|
|
||||||
mkdir -p /go/bin && \
|
|
||||||
mv -v ctop /go/bin/
|
|
||||||
159
Gopkg.lock
generated
Normal file
159
Gopkg.lock
generated
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# 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
|
||||||
47
Gopkg.toml
Normal file
47
Gopkg.toml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
# 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"
|
||||||
21
Makefile
21
Makefile
@@ -5,32 +5,31 @@ 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) -extldflags=$(EXT_LD_FLAGS)"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build/ release/
|
rm -rf _build/ release/
|
||||||
|
|
||||||
build:
|
build:
|
||||||
glide install
|
dep ensure
|
||||||
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
|
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
|
||||||
|
|
||||||
build-dev:
|
build-dev:
|
||||||
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
|
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
|
||||||
|
|
||||||
build-all:
|
build-all:
|
||||||
mkdir -p build
|
mkdir -p _build
|
||||||
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-darwin-amd64
|
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=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=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=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
|
||||||
|
cd _build; sha256sum * > sha256sums.txt
|
||||||
|
|
||||||
image:
|
image:
|
||||||
docker build -t ctop_build -f Dockerfile_build .
|
|
||||||
docker create --name=ctop_built ctop_build ctop -v
|
|
||||||
docker cp ctop_built:/go/bin/ctop .
|
|
||||||
docker build -t ctop -f Dockerfile .
|
docker build -t ctop -f Dockerfile .
|
||||||
|
|
||||||
release:
|
release:
|
||||||
mkdir release
|
mkdir release
|
||||||
go get github.com/progrium/gh-release/...
|
go get github.com/progrium/gh-release/...
|
||||||
cp build/* release
|
cp _build/* release
|
||||||
|
cd release; sha256sum --quiet --check sha256sums.txt
|
||||||
gh-release create bcicen/$(NAME) $(VERSION) \
|
gh-release create bcicen/$(NAME) $(VERSION) \
|
||||||
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
|
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -9,7 +9,7 @@ Top-like interface for container metrics
|
|||||||
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
|
`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>
|
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
|
||||||
|
|
||||||
as well as an [expanded view][expanded_view] for inspecting a specific container.
|
as well as an [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.
|
`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
|
#### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.6/ctop-0.6-linux-amd64 -O /usr/local/bin/ctop
|
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7/ctop-0.7-linux-amd64 -O /usr/local/bin/ctop
|
||||||
sudo chmod +x /usr/local/bin/ctop
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ brew install ctop
|
|||||||
```
|
```
|
||||||
or
|
or
|
||||||
```bash
|
```bash
|
||||||
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6/ctop-0.6-darwin-amd64
|
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7/ctop-0.7-darwin-amd64
|
||||||
sudo chmod +x /usr/local/bin/ctop
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,6 +54,10 @@ Build steps can be found [here][build].
|
|||||||
|
|
||||||
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Option | Description
|
Option | Description
|
||||||
@@ -64,22 +68,27 @@ Option | Description
|
|||||||
-i | invert default colors
|
-i | invert default colors
|
||||||
-r | reverse container sort order
|
-r | reverse container sort order
|
||||||
-s | select initial container sort field
|
-s | select initial container sort field
|
||||||
|
-scale-cpu | show cpu as % of system total
|
||||||
-v | output version information and exit
|
-v | output version information and exit
|
||||||
|
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
Key | Action
|
Key | Action
|
||||||
--- | ---
|
--- | ---
|
||||||
|
<enter> | Open container menu
|
||||||
a | Toggle display of all (running and non-running) containers
|
a | Toggle display of all (running and non-running) containers
|
||||||
f | Filter displayed containers (`esc` to clear when open)
|
f | Filter displayed containers (`esc` to clear when open)
|
||||||
H | Toggle ctop header
|
H | Toggle ctop header
|
||||||
h | Open help dialog
|
h | Open help dialog
|
||||||
s | Select container sort field
|
s | Select container sort field
|
||||||
r | Reverse container sort order
|
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
|
q | Quit ctop
|
||||||
|
|
||||||
[build]: _docs/build.md
|
[build]: _docs/build.md
|
||||||
[connectors]: _docs/connectors.md
|
[connectors]: _docs/connectors.md
|
||||||
[expanded_view]: _docs/expanded.md
|
[single_view]: _docs/single.md
|
||||||
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
|
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
|
||||||
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
|
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Build
|
# Build
|
||||||
|
|
||||||
To build `ctop` from source, ensure you have a recent version of [glide](https://github.com/Masterminds/glide) installed and run:
|
To build `ctop` from source, ensure you have [dep](https://github.com/golang/dep) installed and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/bcicen/ctop && \
|
go get github.com/bcicen/ctop && \
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`
|
|||||||
|
|
||||||
## RunC
|
## RunC
|
||||||
|
|
||||||
Using this connector requires full privileges to the local runC root dir (default: `/run/runc`)
|
Using this connector requires full privileges to the local runC root dir of container state (default: `/run/runc`)
|
||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
|
|
||||||
Var | Description
|
Var | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
RUNC_ROOT | path to runc root (default: `/run/runc`)
|
RUNC_ROOT | path to runc root for container state (default: `/run/runc`)
|
||||||
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups
|
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# Expanded View
|
|
||||||
|
|
||||||
ctop provides an expanded, rolling view for following container metrics
|
|
||||||
<p align="center"><img width="80%" src="img/expanded.gif" alt="ctop"/></p>
|
|
||||||
|
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 549 KiB |
4
_docs/single.md
Normal file
4
_docs/single.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Single Container View
|
||||||
|
|
||||||
|
ctop provides a rolling, single container view for following metrics
|
||||||
|
<p align="center"><img width="80%" src="img/single.gif" alt="ctop"/></p>
|
||||||
23
circle.yml
23
circle.yml
@@ -1,23 +0,0 @@
|
|||||||
machine:
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
environment:
|
|
||||||
IMAGE_NAME: quay.io/vektorlab/ctop
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- docker info
|
|
||||||
- make image
|
|
||||||
|
|
||||||
test:
|
|
||||||
override:
|
|
||||||
- docker run -ti ctop -v
|
|
||||||
|
|
||||||
deployment:
|
|
||||||
hub:
|
|
||||||
branch: master
|
|
||||||
commands:
|
|
||||||
- docker tag ctop ${IMAGE_NAME}:latest
|
|
||||||
- docker tag ctop ${IMAGE_NAME}:$(cat VERSION)
|
|
||||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
|
|
||||||
- docker push ${IMAGE_NAME}
|
|
||||||
@@ -45,6 +45,9 @@ var ColorMap = map[string]ui.Attribute{
|
|||||||
"par.text.hi": ui.ColorBlack,
|
"par.text.hi": ui.ColorBlack,
|
||||||
"sparkline.line.fg": ui.ColorGreen,
|
"sparkline.line.fg": ui.ColorGreen,
|
||||||
"sparkline.title.fg": ui.ColorWhite,
|
"sparkline.title.fg": ui.ColorWhite,
|
||||||
|
"status.ok": ui.ColorGreen,
|
||||||
|
"status.warn": ui.ColorYellow,
|
||||||
|
"status.danger": ui.ColorRed,
|
||||||
}
|
}
|
||||||
|
|
||||||
func InvertColorMap() {
|
func InvertColorMap() {
|
||||||
|
|||||||
119
config/file.go
Normal file
119
config/file.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
xdgRe = regexp.MustCompile("^XDG_*")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigFile struct {
|
||||||
|
Options map[string]string `toml:"options"`
|
||||||
|
Toggles map[string]bool `toml:"toggles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportConfig() ConfigFile {
|
||||||
|
c := ConfigFile{
|
||||||
|
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
|
||||||
|
|
||||||
|
path, err := getConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Write() (path string, err error) {
|
||||||
|
path, err = getConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return path, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgdir := basedir(path)
|
||||||
|
// create config dir if not exist
|
||||||
|
if _, err := os.Stat(cfgdir); err != nil {
|
||||||
|
err = os.MkdirAll(cfgdir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return path, fmt.Errorf("failed to create config dir [%s]: %s", cfgdir, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := toml.NewEncoder(file)
|
||||||
|
err = writer.Encode(exportConfig())
|
||||||
|
if err != nil {
|
||||||
|
return path, fmt.Errorf("failed to write config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine config path from environment
|
||||||
|
func getConfigPath() (path string, err error) {
|
||||||
|
homeDir, ok := os.LookupEnv("HOME")
|
||||||
|
if !ok {
|
||||||
|
return path, fmt.Errorf("$HOME not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// use xdg config home if possible
|
||||||
|
if xdgSupport() {
|
||||||
|
xdgHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
|
||||||
|
if !ok {
|
||||||
|
xdgHome = fmt.Sprintf("%s/.config", homeDir)
|
||||||
|
}
|
||||||
|
path = fmt.Sprintf("%s/ctop/config", xdgHome)
|
||||||
|
} else {
|
||||||
|
path = fmt.Sprintf("%s/.ctop", homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for environemnt supporting XDG spec
|
||||||
|
func xdgSupport() bool {
|
||||||
|
for _, e := range os.Environ() {
|
||||||
|
if xdgRe.FindAllString(e, 1) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func basedir(path string) string {
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
return strings.Join((parts[0 : len(parts)-1]), "/")
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@ var switches = []*Switch{
|
|||||||
Val: true,
|
Val: true,
|
||||||
Label: "Enable Status Header",
|
Label: "Enable Status Header",
|
||||||
},
|
},
|
||||||
|
&Switch{
|
||||||
|
Key: "scaleCpu",
|
||||||
|
Val: false,
|
||||||
|
Label: "Show CPU as %% of system total",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type Switch struct {
|
type Switch struct {
|
||||||
@@ -40,6 +45,14 @@ func GetSwitchVal(k string) bool {
|
|||||||
return GetSwitch(k).Val
|
return GetSwitch(k).Val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateSwitch(k string, val bool) {
|
||||||
|
sw := GetSwitch(k)
|
||||||
|
if sw.Val != val {
|
||||||
|
log.Noticef("config change: %s: %t -> %t", k, sw.Val, val)
|
||||||
|
sw.Val = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle a boolean switch
|
// Toggle a boolean switch
|
||||||
func Toggle(k string) {
|
func Toggle(k string) {
|
||||||
sw := GetSwitch(k)
|
sw := GetSwitch(k)
|
||||||
|
|||||||
@@ -1,33 +1,36 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Docker collector
|
// Docker collector
|
||||||
type Docker struct {
|
type Docker struct {
|
||||||
metrics.Metrics
|
models.Metrics
|
||||||
id string
|
id string
|
||||||
client *api.Client
|
client *api.Client
|
||||||
running bool
|
running bool
|
||||||
stream chan metrics.Metrics
|
stream chan models.Metrics
|
||||||
done chan bool
|
done chan bool
|
||||||
lastCpu float64
|
lastCpu float64
|
||||||
lastSysCpu float64
|
lastSysCpu float64
|
||||||
|
scaleCpu bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocker(client *api.Client, id string) *Docker {
|
func NewDocker(client *api.Client, id string) *Docker {
|
||||||
return &Docker{
|
return &Docker{
|
||||||
Metrics: metrics.Metrics{},
|
Metrics: models.Metrics{},
|
||||||
id: id,
|
id: id,
|
||||||
client: client,
|
client: client,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Start() {
|
func (c *Docker) Start() {
|
||||||
c.done = make(chan bool)
|
c.done = make(chan bool)
|
||||||
c.stream = make(chan metrics.Metrics)
|
c.stream = make(chan models.Metrics)
|
||||||
stats := make(chan *api.Stats)
|
stats := make(chan *api.Stats)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -61,10 +64,14 @@ func (c *Docker) Running() bool {
|
|||||||
return c.running
|
return c.running
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Stream() chan metrics.Metrics {
|
func (c *Docker) Stream() chan models.Metrics {
|
||||||
return c.stream
|
return c.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Docker) Logs() LogCollector {
|
||||||
|
return NewDockerLogs(c.id, c.client)
|
||||||
|
}
|
||||||
|
|
||||||
// Stop collector
|
// Stop collector
|
||||||
func (c *Docker) Stop() {
|
func (c *Docker) Stop() {
|
||||||
c.done <- true
|
c.done <- true
|
||||||
@@ -78,14 +85,18 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
|
|||||||
cpudiff := total - c.lastCpu
|
cpudiff := total - c.lastCpu
|
||||||
syscpudiff := system - c.lastSysCpu
|
syscpudiff := system - c.lastSysCpu
|
||||||
|
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||||
|
}
|
||||||
c.lastCpu = total
|
c.lastCpu = total
|
||||||
c.lastSysCpu = system
|
c.lastSysCpu = system
|
||||||
c.Pids = int(stats.PidsStats.Current)
|
c.Pids = int(stats.PidsStats.Current)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) ReadMem(stats *api.Stats) {
|
func (c *Docker) ReadMem(stats *api.Stats) {
|
||||||
c.MemUsage = int64(stats.MemoryStats.Usage)
|
c.MemUsage = int64(stats.MemoryStats.Usage - stats.MemoryStats.Stats.Cache)
|
||||||
c.MemLimit = int64(stats.MemoryStats.Limit)
|
c.MemLimit = int64(stats.MemoryStats.Limit)
|
||||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
}
|
}
|
||||||
|
|||||||
84
connector/collector/docker_logs.go
Normal file
84
connector/collector/docker_logs.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
api "github.com/fsouza/go-dockerclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerLogs struct {
|
||||||
|
id string
|
||||||
|
client *api.Client
|
||||||
|
done chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerLogs(id string, client *api.Client) *DockerLogs {
|
||||||
|
return &DockerLogs{
|
||||||
|
id: id,
|
||||||
|
client: client,
|
||||||
|
done: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DockerLogs) Stream() chan models.Log {
|
||||||
|
r, w := io.Pipe()
|
||||||
|
logCh := make(chan models.Log)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
opts := api.LogsOptions{
|
||||||
|
Context: ctx,
|
||||||
|
Container: l.id,
|
||||||
|
OutputStream: w,
|
||||||
|
ErrorStream: w,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
Tail: "10",
|
||||||
|
Follow: true,
|
||||||
|
Timestamps: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// read io pipe into channel
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
parts := strings.Split(scanner.Text(), " ")
|
||||||
|
ts := l.parseTime(parts[0])
|
||||||
|
logCh <- models.Log{ts, strings.Join(parts[1:], " ")}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// connect to container log stream
|
||||||
|
go func() {
|
||||||
|
err := l.client.Logs(opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error reading container logs: %s", err)
|
||||||
|
}
|
||||||
|
log.Infof("log reader stopped for container: %s", l.id)
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-l.done:
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("log reader started for container: %s", l.id)
|
||||||
|
return logCh
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
return ts
|
||||||
|
}
|
||||||
@@ -4,10 +4,24 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.Init()
|
var log = logging.Init()
|
||||||
|
|
||||||
|
type LogCollector interface {
|
||||||
|
Stream() chan models.Log
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Collector interface {
|
||||||
|
Stream() chan models.Metrics
|
||||||
|
Logs() LogCollector
|
||||||
|
Running() bool
|
||||||
|
Start()
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
func round(num float64) int {
|
func round(num float64) int {
|
||||||
return int(num + math.Copysign(0.5, num))
|
return int(num + math.Copysign(0.5, num))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mock collector
|
// Mock collector
|
||||||
type Mock struct {
|
type Mock struct {
|
||||||
metrics.Metrics
|
models.Metrics
|
||||||
stream chan metrics.Metrics
|
stream chan models.Metrics
|
||||||
done bool
|
done bool
|
||||||
running bool
|
running bool
|
||||||
aggression int64
|
aggression int64
|
||||||
@@ -20,7 +20,7 @@ type Mock struct {
|
|||||||
|
|
||||||
func NewMock(a int64) *Mock {
|
func NewMock(a int64) *Mock {
|
||||||
c := &Mock{
|
c := &Mock{
|
||||||
Metrics: metrics.Metrics{},
|
Metrics: models.Metrics{},
|
||||||
aggression: a,
|
aggression: a,
|
||||||
}
|
}
|
||||||
c.MemLimit = 2147483648
|
c.MemLimit = 2147483648
|
||||||
@@ -33,7 +33,7 @@ func (c *Mock) Running() bool {
|
|||||||
|
|
||||||
func (c *Mock) Start() {
|
func (c *Mock) Start() {
|
||||||
c.done = false
|
c.done = false
|
||||||
c.stream = make(chan metrics.Metrics)
|
c.stream = make(chan models.Metrics)
|
||||||
go c.run()
|
go c.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,10 +41,14 @@ func (c *Mock) Stop() {
|
|||||||
c.done = true
|
c.done = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Mock) Stream() chan metrics.Metrics {
|
func (c *Mock) Stream() chan models.Metrics {
|
||||||
return c.stream
|
return c.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Mock) Logs() LogCollector {
|
||||||
|
return &MockLogs{make(chan bool)}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Mock) run() {
|
func (c *Mock) run() {
|
||||||
c.running = true
|
c.running = true
|
||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
rand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
|||||||
31
connector/collector/mock_logs.go
Normal file
31
connector/collector/mock_logs.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea, regem falli contexo pro peregrinorum heremo absconditi araneae meminerim deliciosas actionibus facere modico dura sonuerunt psalmi contra rerum, tempus mala anima volebant dura quae o modis."
|
||||||
|
|
||||||
|
type MockLogs struct {
|
||||||
|
done chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *MockLogs) Stream() chan models.Log {
|
||||||
|
logCh := make(chan models.Log)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-l.done:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
logCh <- models.Log{time.Now(), mockLog}
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return logCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *MockLogs) Stop() { l.done <- true }
|
||||||
@@ -5,30 +5,33 @@ package collector
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
"github.com/opencontainers/runc/libcontainer"
|
"github.com/opencontainers/runc/libcontainer"
|
||||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Runc collector
|
// Runc collector
|
||||||
type Runc struct {
|
type Runc struct {
|
||||||
metrics.Metrics
|
models.Metrics
|
||||||
id string
|
id string
|
||||||
libc libcontainer.Container
|
libc libcontainer.Container
|
||||||
stream chan metrics.Metrics
|
stream chan models.Metrics
|
||||||
done bool
|
done bool
|
||||||
running bool
|
running bool
|
||||||
interval int // collection interval, in seconds
|
interval int // collection interval, in seconds
|
||||||
lastCpu float64
|
lastCpu float64
|
||||||
lastSysCpu float64
|
lastSysCpu float64
|
||||||
|
scaleCpu bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunc(libc libcontainer.Container) *Runc {
|
func NewRunc(libc libcontainer.Container) *Runc {
|
||||||
c := &Runc{
|
c := &Runc{
|
||||||
Metrics: metrics.Metrics{},
|
Metrics: models.Metrics{},
|
||||||
id: libc.ID(),
|
id: libc.ID(),
|
||||||
libc: libc,
|
libc: libc,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,7 @@ func (c *Runc) Running() bool {
|
|||||||
|
|
||||||
func (c *Runc) Start() {
|
func (c *Runc) Start() {
|
||||||
c.done = false
|
c.done = false
|
||||||
c.stream = make(chan metrics.Metrics)
|
c.stream = make(chan models.Metrics)
|
||||||
go c.run()
|
go c.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +50,14 @@ func (c *Runc) Stop() {
|
|||||||
c.done = true
|
c.done = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Runc) Stream() chan metrics.Metrics {
|
func (c *Runc) Stream() chan models.Metrics {
|
||||||
return c.stream
|
return c.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Runc) Logs() LogCollector {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Runc) run() {
|
func (c *Runc) run() {
|
||||||
c.running = true
|
c.running = true
|
||||||
defer close(c.stream)
|
defer close(c.stream)
|
||||||
@@ -86,7 +93,11 @@ func (c *Runc) ReadCPU(stats *cgroups.Stats) {
|
|||||||
cpudiff := total - c.lastCpu
|
cpudiff := total - c.lastCpu
|
||||||
syscpudiff := system - c.lastSysCpu
|
syscpudiff := system - c.lastSysCpu
|
||||||
|
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||||
|
}
|
||||||
c.lastCpu = total
|
c.lastCpu = total
|
||||||
c.lastSysCpu = system
|
c.lastSysCpu = system
|
||||||
c.Pids = int(stats.PidsStats.Current)
|
c.Pids = int(stats.PidsStats.Current)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
@@ -45,8 +46,11 @@ func (cm *Docker) watchEvents() {
|
|||||||
if e.Type != "container" {
|
if e.Type != "container" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch e.Action {
|
|
||||||
case "start", "die", "pause", "unpause":
|
actionName := strings.Split(e.Action, ":")[0]
|
||||||
|
|
||||||
|
switch actionName {
|
||||||
|
case "start", "die", "pause", "unpause", "health_status":
|
||||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||||
cm.needsRefresh <- e.ID
|
cm.needsRefresh <- e.ID
|
||||||
case "destroy":
|
case "destroy":
|
||||||
@@ -66,7 +70,7 @@ func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, binding := range v {
|
for _, binding := range v {
|
||||||
s := fmt.Sprintf("%s -> %s:%s", k, binding.HostIP, binding.HostPort)
|
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
|
||||||
published = append(published, s)
|
published = append(published, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +89,7 @@ func (cm *Docker) refresh(c *container.Container) {
|
|||||||
c.SetMeta("image", insp.Config.Image)
|
c.SetMeta("image", insp.Config.Image)
|
||||||
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
||||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
||||||
|
c.SetMeta("health", insp.State.Health.Status)
|
||||||
c.SetState(insp.State.Status)
|
c.SetState(insp.State.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,8 +133,10 @@ func (cm *Docker) MustGet(id string) *container.Container {
|
|||||||
if !ok {
|
if !ok {
|
||||||
// create collector
|
// create collector
|
||||||
collector := collector.NewDocker(cm.client, id)
|
collector := collector.NewDocker(cm.client, id)
|
||||||
|
// create manager
|
||||||
|
manager := manager.NewDocker(cm.client, id)
|
||||||
// create container
|
// create container
|
||||||
c = container.New(id, collector)
|
c = container.New(id, collector, manager)
|
||||||
cm.lock.Lock()
|
cm.lock.Lock()
|
||||||
cm.containers[id] = c
|
cm.containers[id] = c
|
||||||
cm.lock.Unlock()
|
cm.lock.Unlock()
|
||||||
@@ -159,6 +166,7 @@ func (cm *Docker) All() (containers container.Containers) {
|
|||||||
for _, c := range cm.containers {
|
for _, c := range cm.containers {
|
||||||
containers = append(containers, c)
|
containers = append(containers, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
containers.Sort()
|
containers.Sort()
|
||||||
containers.Filter()
|
containers.Filter()
|
||||||
cm.lock.Unlock()
|
cm.lock.Unlock()
|
||||||
|
|||||||
44
connector/manager/docker.go
Normal file
44
connector/manager/docker.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
api "github.com/fsouza/go-dockerclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Docker struct {
|
||||||
|
id string
|
||||||
|
client *api.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocker(client *api.Client, id string) *Docker {
|
||||||
|
return &Docker{
|
||||||
|
id: id,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Docker) Start() error {
|
||||||
|
c, err := dc.client.InspectContainer(dc.id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot inspect container: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dc.client.StartContainer(c.ID, c.HostConfig); err != nil {
|
||||||
|
return fmt.Errorf("cannot start container: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Docker) Stop() error {
|
||||||
|
if err := dc.client.StopContainer(dc.id, 3); err != nil {
|
||||||
|
return fmt.Errorf("cannot stop container: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *Docker) Remove() error {
|
||||||
|
if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
|
||||||
|
return fmt.Errorf("cannot remove container: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
7
connector/manager/main.go
Normal file
7
connector/manager/main.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
type Manager interface {
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
Remove() error
|
||||||
|
}
|
||||||
19
connector/manager/mock.go
Normal file
19
connector/manager/mock.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
type Mock struct{}
|
||||||
|
|
||||||
|
func NewMock() *Mock {
|
||||||
|
return &Mock{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mock) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mock) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mock) Remove() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
19
connector/manager/runc.go
Normal file
19
connector/manager/runc.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
type Runc struct{}
|
||||||
|
|
||||||
|
func NewRunc() *Runc {
|
||||||
|
return &Runc{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Runc) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Runc) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Runc) Remove() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/jgautheron/codename-generator"
|
"github.com/jgautheron/codename-generator"
|
||||||
"github.com/nu7hatch/gouuid"
|
"github.com/nu7hatch/gouuid"
|
||||||
@@ -40,7 +41,8 @@ func (cs *Mock) Init() {
|
|||||||
|
|
||||||
func (cs *Mock) makeContainer(aggression int64) {
|
func (cs *Mock) makeContainer(aggression int64) {
|
||||||
collector := collector.NewMock(aggression)
|
collector := collector.NewMock(aggression)
|
||||||
c := container.New(makeID(), collector)
|
manager := manager.NewMock()
|
||||||
|
c := container.New(makeID(), collector, manager)
|
||||||
c.SetMeta("name", makeName())
|
c.SetMeta("name", makeName())
|
||||||
c.SetState(makeState())
|
c.SetState(makeState())
|
||||||
cs.containers = append(cs.containers, c)
|
cs.containers = append(cs.containers, c)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/opencontainers/runc/libcontainer"
|
"github.com/opencontainers/runc/libcontainer"
|
||||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||||
@@ -175,7 +176,8 @@ func (cm *Runc) MustGet(id string) *container.Container {
|
|||||||
collector := collector.NewRunc(libc)
|
collector := collector.NewRunc(libc)
|
||||||
|
|
||||||
// create container
|
// create container
|
||||||
c = container.New(id, collector)
|
manager := manager.NewRunc()
|
||||||
|
c = container.New(id, collector, manager)
|
||||||
|
|
||||||
name := libc.ID()
|
name := libc.ID()
|
||||||
// set initial metadata
|
// set initial metadata
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/cwidgets"
|
"github.com/bcicen/ctop/cwidgets"
|
||||||
"github.com/bcicen/ctop/cwidgets/compact"
|
"github.com/bcicen/ctop/cwidgets/compact"
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -13,24 +15,26 @@ var (
|
|||||||
|
|
||||||
// Metrics and metadata representing a container
|
// Metrics and metadata representing a container
|
||||||
type Container struct {
|
type Container struct {
|
||||||
metrics.Metrics
|
models.Metrics
|
||||||
Id string
|
Id string
|
||||||
Meta map[string]string
|
Meta map[string]string
|
||||||
Widgets *compact.Compact
|
Widgets *compact.Compact
|
||||||
Display bool // display this container in compact view
|
Display bool // display this container in compact view
|
||||||
updater cwidgets.WidgetUpdater
|
updater cwidgets.WidgetUpdater
|
||||||
collector metrics.Collector
|
collector collector.Collector
|
||||||
|
manager manager.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(id string, collector metrics.Collector) *Container {
|
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
|
||||||
widgets := compact.NewCompact(id)
|
widgets := compact.NewCompact(id)
|
||||||
return &Container{
|
return &Container{
|
||||||
Metrics: metrics.NewMetrics(),
|
Metrics: models.NewMetrics(),
|
||||||
Id: id,
|
Id: id,
|
||||||
Meta: make(map[string]string),
|
Meta: make(map[string]string),
|
||||||
Widgets: widgets,
|
Widgets: widgets,
|
||||||
updater: widgets,
|
updater: widgets,
|
||||||
collector: collector,
|
collector: collector,
|
||||||
|
manager: manager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +70,50 @@ func (c *Container) SetState(s string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return container log collector
|
||||||
|
func (c *Container) Logs() collector.LogCollector {
|
||||||
|
return c.collector.Logs()
|
||||||
|
}
|
||||||
|
|
||||||
// Read metric stream, updating widgets
|
// Read metric stream, updating widgets
|
||||||
func (c *Container) Read(stream chan metrics.Metrics) {
|
func (c *Container) Read(stream chan models.Metrics) {
|
||||||
go func() {
|
go func() {
|
||||||
for metrics := range stream {
|
for metrics := range stream {
|
||||||
c.Metrics = metrics
|
c.Metrics = metrics
|
||||||
c.updater.SetMetrics(metrics)
|
c.updater.SetMetrics(metrics)
|
||||||
}
|
}
|
||||||
log.Infof("reader stopped for container: %s", c.Id)
|
log.Infof("reader stopped for container: %s", c.Id)
|
||||||
c.Metrics = metrics.NewMetrics()
|
c.Metrics = models.NewMetrics()
|
||||||
c.Widgets.Reset()
|
c.Widgets.Reset()
|
||||||
}()
|
}()
|
||||||
log.Infof("reader started for container: %s", c.Id)
|
log.Infof("reader started for container: %s", c.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) Start() {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Stop() {
|
||||||
|
if c.Meta["state"] == "running" {
|
||||||
|
if err := c.manager.Stop(); err != nil {
|
||||||
|
log.Warningf("container %s: %v", c.Id, err)
|
||||||
|
log.StatusErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetState("exited")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Remove() {
|
||||||
|
if err := c.manager.Remove(); err != nil {
|
||||||
|
log.Warningf("container %s: %v", c.Id, err)
|
||||||
|
log.StatusErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
cursor.go
28
cursor.go
@@ -142,10 +142,11 @@ func (gc *GridCursor) PgUp() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextidx int
|
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
|
||||||
nextidx = int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
|
if gc.pgCount() > 0 {
|
||||||
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
|
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
|
||||||
float64(0)))
|
float64(0)))
|
||||||
|
}
|
||||||
|
|
||||||
active := gc.filtered[idx]
|
active := gc.filtered[idx]
|
||||||
next := gc.filtered[nextidx]
|
next := gc.filtered[nextidx]
|
||||||
@@ -164,11 +165,11 @@ func (gc *GridCursor) PgDown() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextidx int
|
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
|
||||||
nextidx = int(math.Min(float64(gc.Len()-1),
|
if gc.pgCount() > 0 {
|
||||||
float64(idx+cGrid.MaxRows())))
|
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
|
||||||
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
|
float64(gc.Len()-cGrid.MaxRows())))
|
||||||
float64(gc.Len()-cGrid.MaxRows())))
|
}
|
||||||
|
|
||||||
active := gc.filtered[idx]
|
active := gc.filtered[idx]
|
||||||
next := gc.filtered[nextidx]
|
next := gc.filtered[nextidx]
|
||||||
@@ -180,3 +181,12 @@ func (gc *GridCursor) PgDown() {
|
|||||||
cGrid.Align()
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// number of pages at current row count and term height
|
||||||
|
func (gc *GridCursor) pgCount() int {
|
||||||
|
pages := gc.Len() / cGrid.MaxRows()
|
||||||
|
if gc.Len()%cGrid.MaxRows() > 0 {
|
||||||
|
pages++
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ func (w *GaugeCol) Reset() {
|
|||||||
|
|
||||||
func colorScale(n int) ui.Attribute {
|
func colorScale(n int) ui.Attribute {
|
||||||
if n > 70 {
|
if n > 70 {
|
||||||
return ui.ColorRed
|
return ui.ThemeAttr("status.danger")
|
||||||
}
|
}
|
||||||
if n > 30 {
|
if n > 30 {
|
||||||
return ui.ColorYellow
|
return ui.ThemeAttr("status.warn")
|
||||||
}
|
}
|
||||||
return ui.ColorGreen
|
return ui.ThemeAttr("status.ok")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ func NewCompactGrid() *CompactGrid {
|
|||||||
|
|
||||||
func (cg *CompactGrid) Align() {
|
func (cg *CompactGrid) Align() {
|
||||||
y := cg.Y
|
y := cg.Y
|
||||||
|
|
||||||
if cg.Offset >= len(cg.Rows) {
|
if cg.Offset >= len(cg.Rows) {
|
||||||
cg.Offset = 0
|
cg.Offset = 0
|
||||||
}
|
}
|
||||||
|
if cg.Offset < 0 {
|
||||||
|
cg.Offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
// update row ypos, width recursively
|
// update row ypos, width recursively
|
||||||
for _, r := range cg.pageRows() {
|
for _, r := range cg.pageRows() {
|
||||||
r.SetY(y)
|
r.SetY(y)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package compact
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ type Compact struct {
|
|||||||
Name *TextCol
|
Name *TextCol
|
||||||
Cid *TextCol
|
Cid *TextCol
|
||||||
Cpu *GaugeCol
|
Cpu *GaugeCol
|
||||||
Memory *GaugeCol
|
Mem *GaugeCol
|
||||||
Net *TextCol
|
Net *TextCol
|
||||||
IO *TextCol
|
IO *TextCol
|
||||||
Pids *TextCol
|
Pids *TextCol
|
||||||
@@ -32,7 +32,7 @@ func NewCompact(id string) *Compact {
|
|||||||
Name: NewTextCol("-"),
|
Name: NewTextCol("-"),
|
||||||
Cid: NewTextCol(id),
|
Cid: NewTextCol(id),
|
||||||
Cpu: NewGaugeCol(),
|
Cpu: NewGaugeCol(),
|
||||||
Memory: NewGaugeCol(),
|
Mem: NewGaugeCol(),
|
||||||
Net: NewTextCol("-"),
|
Net: NewTextCol("-"),
|
||||||
IO: NewTextCol("-"),
|
IO: NewTextCol("-"),
|
||||||
Pids: NewTextCol("-"),
|
Pids: NewTextCol("-"),
|
||||||
@@ -56,10 +56,12 @@ func (row *Compact) SetMeta(k, v string) {
|
|||||||
row.Name.Set(v)
|
row.Name.Set(v)
|
||||||
case "state":
|
case "state":
|
||||||
row.Status.Set(v)
|
row.Status.Set(v)
|
||||||
|
case "health":
|
||||||
|
row.Status.SetHealth(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row *Compact) SetMetrics(m metrics.Metrics) {
|
func (row *Compact) SetMetrics(m models.Metrics) {
|
||||||
row.SetCPU(m.CPUUtil)
|
row.SetCPU(m.CPUUtil)
|
||||||
row.SetNet(m.NetRx, m.NetTx)
|
row.SetNet(m.NetRx, m.NetTx)
|
||||||
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
||||||
@@ -70,7 +72,7 @@ func (row *Compact) SetMetrics(m metrics.Metrics) {
|
|||||||
// Set gauges, counters to default unread values
|
// Set gauges, counters to default unread values
|
||||||
func (row *Compact) Reset() {
|
func (row *Compact) Reset() {
|
||||||
row.Cpu.Reset()
|
row.Cpu.Reset()
|
||||||
row.Memory.Reset()
|
row.Mem.Reset()
|
||||||
row.Net.Reset()
|
row.Net.Reset()
|
||||||
row.IO.Reset()
|
row.IO.Reset()
|
||||||
row.Pids.Reset()
|
row.Pids.Reset()
|
||||||
@@ -121,7 +123,7 @@ func (row *Compact) Buffer() ui.Buffer {
|
|||||||
buf.Merge(row.Name.Buffer())
|
buf.Merge(row.Name.Buffer())
|
||||||
buf.Merge(row.Cid.Buffer())
|
buf.Merge(row.Cid.Buffer())
|
||||||
buf.Merge(row.Cpu.Buffer())
|
buf.Merge(row.Cpu.Buffer())
|
||||||
buf.Merge(row.Memory.Buffer())
|
buf.Merge(row.Mem.Buffer())
|
||||||
buf.Merge(row.Net.Buffer())
|
buf.Merge(row.Net.Buffer())
|
||||||
buf.Merge(row.IO.Buffer())
|
buf.Merge(row.IO.Buffer())
|
||||||
buf.Merge(row.Pids.Buffer())
|
buf.Merge(row.Pids.Buffer())
|
||||||
@@ -134,7 +136,7 @@ func (row *Compact) all() []ui.GridBufferer {
|
|||||||
row.Name,
|
row.Name,
|
||||||
row.Cid,
|
row.Cid,
|
||||||
row.Cpu,
|
row.Cpu,
|
||||||
row.Memory,
|
row.Mem,
|
||||||
row.Net,
|
row.Net,
|
||||||
row.IO,
|
row.IO,
|
||||||
row.Pids,
|
row.Pids,
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ func (row *Compact) SetCPU(val int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (row *Compact) SetMem(val int64, limit int64, percent int) {
|
func (row *Compact) SetMem(val int64, limit int64, percent int) {
|
||||||
row.Memory.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
|
row.Mem.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
|
||||||
if percent < 5 {
|
if percent < 5 {
|
||||||
percent = 5
|
percent = 5
|
||||||
row.Memory.BarColor = ui.ColorBlack
|
row.Mem.BarColor = ui.ColorBlack
|
||||||
} else {
|
} else {
|
||||||
row.Memory.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
row.Mem.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||||
}
|
}
|
||||||
row.Memory.Percent = percent
|
row.Mem.Percent = percent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
package compact
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mark = string('\u25C9')
|
mark = string('\u25C9')
|
||||||
vBar = string('\u25AE')
|
healthMark = string('\u207A')
|
||||||
statusWidth = 3
|
vBar = string('\u25AE') + string('\u25AE')
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status indicator
|
// Status indicator
|
||||||
type Status struct {
|
type Status struct {
|
||||||
*ui.Par
|
*ui.Block
|
||||||
|
status []ui.Cell
|
||||||
|
health []ui.Cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatus() *Status {
|
func NewStatus() *Status {
|
||||||
p := ui.NewPar(mark)
|
s := &Status{Block: ui.NewBlock()}
|
||||||
p.Border = false
|
s.Height = 1
|
||||||
p.Height = 1
|
s.Border = false
|
||||||
p.Width = statusWidth
|
s.Set("")
|
||||||
return &Status{p}
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Status) Buffer() ui.Buffer {
|
||||||
|
buf := s.Block.Buffer()
|
||||||
|
x := 0
|
||||||
|
for _, c := range s.status {
|
||||||
|
buf.Set(s.InnerX()+x, s.InnerY(), c)
|
||||||
|
x += c.Width()
|
||||||
|
}
|
||||||
|
for _, c := range s.health {
|
||||||
|
buf.Set(s.InnerX()+x, s.InnerY(), c)
|
||||||
|
x += c.Width()
|
||||||
|
}
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Status) Set(val string) {
|
func (s *Status) Set(val string) {
|
||||||
@@ -32,13 +46,38 @@ func (s *Status) Set(val string) {
|
|||||||
|
|
||||||
switch val {
|
switch val {
|
||||||
case "running":
|
case "running":
|
||||||
color = ui.ColorGreen
|
color = ui.ThemeAttr("status.ok")
|
||||||
case "exited":
|
case "exited":
|
||||||
color = ui.ColorRed
|
color = ui.ThemeAttr("status.danger")
|
||||||
case "paused":
|
case "paused":
|
||||||
text = fmt.Sprintf("%s%s", vBar, vBar)
|
text = vBar
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Text = text
|
var cells []ui.Cell
|
||||||
s.TextFgColor = color
|
for _, ch := range text {
|
||||||
|
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
|
||||||
|
}
|
||||||
|
s.status = cells
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Status) SetHealth(val string) {
|
||||||
|
if val == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
color := ui.ColorDefault
|
||||||
|
|
||||||
|
switch val {
|
||||||
|
case "healthy":
|
||||||
|
color = ui.ThemeAttr("status.ok")
|
||||||
|
case "unhealthy":
|
||||||
|
color = ui.ThemeAttr("status.danger")
|
||||||
|
case "starting":
|
||||||
|
color = ui.ThemeAttr("status.warn")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cells []ui.Cell
|
||||||
|
for _, ch := range healthMark {
|
||||||
|
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
|
||||||
|
}
|
||||||
|
s.health = cells
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package cwidgets
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.Init()
|
var log = logging.Init()
|
||||||
|
|
||||||
type WidgetUpdater interface {
|
type WidgetUpdater interface {
|
||||||
SetMeta(string, string)
|
SetMeta(string, string)
|
||||||
SetMetrics(metrics.Metrics)
|
SetMetrics(models.Metrics)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
type IntHist struct {
|
type IntHist struct {
|
||||||
Val int // most current data point
|
Val int // most current data point
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var displayInfo = []string{"id", "name", "image", "ports", "state", "created"}
|
var displayInfo = []string{"id", "name", "image", "ports", "state", "created", "health"}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
*ui.Table
|
*ui.Table
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
83
cwidgets/single/logs.go
Normal file
83
cwidgets/single/logs.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package single
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogLines struct {
|
||||||
|
ts []time.Time
|
||||||
|
data []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogLines(max int) *LogLines {
|
||||||
|
ll := &LogLines{
|
||||||
|
ts: make([]time.Time, max),
|
||||||
|
data: make([]string, max),
|
||||||
|
}
|
||||||
|
return ll
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ll *LogLines) tail(n int) []string {
|
||||||
|
lines := make([]string, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
lines = append(lines, ll.data[len(ll.data)-i])
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
func (ll *LogLines) getLines(start, end int) []string {
|
||||||
|
if end < 0 {
|
||||||
|
return ll.data[start:]
|
||||||
|
}
|
||||||
|
return ll.data[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ll *LogLines) add(l models.Log) {
|
||||||
|
if len(ll.data) == cap(ll.data) {
|
||||||
|
ll.data = append(ll.data[:0], ll.data[1:]...)
|
||||||
|
ll.ts = append(ll.ts[:0], ll.ts[1:]...)
|
||||||
|
}
|
||||||
|
ll.ts = append(ll.ts, l.Timestamp)
|
||||||
|
ll.data = append(ll.data, l.Message)
|
||||||
|
log.Debugf("recorded log line: %v", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logs struct {
|
||||||
|
*ui.List
|
||||||
|
lines *LogLines
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogs(stream chan models.Log) *Logs {
|
||||||
|
p := ui.NewList()
|
||||||
|
p.Y = ui.TermHeight() / 2
|
||||||
|
p.X = 0
|
||||||
|
p.Height = ui.TermHeight() - p.Y
|
||||||
|
p.Width = ui.TermWidth()
|
||||||
|
//p.Overflow = "wrap"
|
||||||
|
p.ItemFgColor = ui.ThemeAttr("par.text.fg")
|
||||||
|
i := &Logs{p, NewLogLines(4098)}
|
||||||
|
go func() {
|
||||||
|
for line := range stream {
|
||||||
|
i.lines.add(line)
|
||||||
|
ui.Render(i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Logs) Align() {
|
||||||
|
w.X = colWidth[0]
|
||||||
|
w.List.Align()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Logs) Buffer() ui.Buffer {
|
||||||
|
maxLines := w.Height - 2
|
||||||
|
offset := len(w.lines.data) - maxLines
|
||||||
|
w.Items = w.lines.getLines(offset, -1)
|
||||||
|
return w.List.Buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of rows a line will occupy at current panel width
|
||||||
|
func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
"github.com/bcicen/ctop/metrics"
|
"github.com/bcicen/ctop/models"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ var (
|
|||||||
colWidth = [2]int{65, 0} // left,right column width
|
colWidth = [2]int{65, 0} // left,right column width
|
||||||
)
|
)
|
||||||
|
|
||||||
type Expanded struct {
|
type Single struct {
|
||||||
Info *Info
|
Info *Info
|
||||||
Net *Net
|
Net *Net
|
||||||
Cpu *Cpu
|
Cpu *Cpu
|
||||||
@@ -22,11 +22,11 @@ type Expanded struct {
|
|||||||
Width int
|
Width int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpanded(id string) *Expanded {
|
func NewSingle(id string) *Single {
|
||||||
if len(id) > 12 {
|
if len(id) > 12 {
|
||||||
id = id[:12]
|
id = id[:12]
|
||||||
}
|
}
|
||||||
return &Expanded{
|
return &Single{
|
||||||
Info: NewInfo(id),
|
Info: NewInfo(id),
|
||||||
Net: NewNet(),
|
Net: NewNet(),
|
||||||
Cpu: NewCpu(),
|
Cpu: NewCpu(),
|
||||||
@@ -36,7 +36,7 @@ func NewExpanded(id string) *Expanded {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Up() {
|
func (e *Single) Up() {
|
||||||
if e.Y < 0 {
|
if e.Y < 0 {
|
||||||
e.Y++
|
e.Y++
|
||||||
e.Align()
|
e.Align()
|
||||||
@@ -44,7 +44,7 @@ func (e *Expanded) Up() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Down() {
|
func (e *Single) Down() {
|
||||||
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||||
e.Y--
|
e.Y--
|
||||||
e.Align()
|
e.Align()
|
||||||
@@ -52,10 +52,10 @@ func (e *Expanded) Down() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetWidth(w int) { e.Width = w }
|
func (e *Single) SetWidth(w int) { e.Width = w }
|
||||||
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
|
func (e *Single) SetMeta(k, v string) { e.Info.Set(k, v) }
|
||||||
|
|
||||||
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
func (e *Single) SetMetrics(m models.Metrics) {
|
||||||
e.Cpu.Update(m.CPUUtil)
|
e.Cpu.Update(m.CPUUtil)
|
||||||
e.Net.Update(m.NetRx, m.NetTx)
|
e.Net.Update(m.NetRx, m.NetTx)
|
||||||
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
||||||
@@ -63,7 +63,7 @@ func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return total column height
|
// Return total column height
|
||||||
func (e *Expanded) GetHeight() (h int) {
|
func (e *Single) GetHeight() (h int) {
|
||||||
h += e.Info.Height
|
h += e.Info.Height
|
||||||
h += e.Net.Height
|
h += e.Net.Height
|
||||||
h += e.Cpu.Height
|
h += e.Cpu.Height
|
||||||
@@ -72,7 +72,7 @@ func (e *Expanded) GetHeight() (h int) {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Align() {
|
func (e *Single) Align() {
|
||||||
// reset offset if needed
|
// reset offset if needed
|
||||||
if e.GetHeight() <= ui.TermHeight() {
|
if e.GetHeight() <= ui.TermHeight() {
|
||||||
e.Y = 0
|
e.Y = 0
|
||||||
@@ -91,10 +91,7 @@ func (e *Expanded) Align() {
|
|||||||
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcWidth(w int) {
|
func (e *Single) Buffer() ui.Buffer {
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Expanded) Buffer() ui.Buffer {
|
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
if e.Width < (colWidth[0] + colWidth[1]) {
|
if e.Width < (colWidth[0] + colWidth[1]) {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
@@ -109,7 +106,7 @@ func (e *Expanded) Buffer() ui.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) all() []ui.GridBufferer {
|
func (e *Single) all() []ui.GridBufferer {
|
||||||
return []ui.GridBufferer{
|
return []ui.GridBufferer{
|
||||||
e.Info,
|
e.Info,
|
||||||
e.Cpu,
|
e.Cpu,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
19
debug.go
19
debug.go
@@ -3,11 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var mstats = &runtime.MemStats{}
|
||||||
|
|
||||||
func logEvent(e ui.Event) {
|
func logEvent(e ui.Event) {
|
||||||
var s string
|
var s string
|
||||||
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
||||||
@@ -19,6 +22,22 @@ func logEvent(e ui.Event) {
|
|||||||
log.Debugf("new event: %s", s)
|
log.Debugf("new event: %s", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runtimeStats() {
|
||||||
|
var msg string
|
||||||
|
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
|
||||||
|
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
|
||||||
|
runtime.ReadMemStats(mstats)
|
||||||
|
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
|
||||||
|
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
|
||||||
|
log.Debugf("runtime: %v", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeStack() {
|
||||||
|
buf := make([]byte, 32768)
|
||||||
|
buf = buf[:runtime.Stack(buf, true)]
|
||||||
|
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
|
||||||
|
}
|
||||||
|
|
||||||
// log container, metrics, and widget state
|
// log container, metrics, and widget state
|
||||||
func dumpContainer(c *container.Container) {
|
func dumpContainer(c *container.Container) {
|
||||||
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
||||||
|
|||||||
129
glide.lock
generated
129
glide.lock
generated
@@ -1,129 +0,0 @@
|
|||||||
hash: 0d550b01b3a1c4751a8f5c3fba0c43f62252055e231712729628e514bb494da8
|
|
||||||
updated: 2017-06-09T18:11:10.930196504-03:00
|
|
||||||
imports:
|
|
||||||
- name: github.com/Azure/go-ansiterm
|
|
||||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
|
||||||
subpackages:
|
|
||||||
- winterm
|
|
||||||
- name: github.com/c9s/goprocinfo
|
|
||||||
version: b34328d6e0cd139894ea7347d2624ccf31fa3c58
|
|
||||||
subpackages:
|
|
||||||
- linux
|
|
||||||
- name: github.com/coreos/go-systemd
|
|
||||||
version: b4a58d95188dd092ae20072bac14cece0e67c388
|
|
||||||
subpackages:
|
|
||||||
- activation
|
|
||||||
- dbus
|
|
||||||
- util
|
|
||||||
- name: github.com/docker/docker
|
|
||||||
version: 90d35abf7b3535c1c319c872900fbd76374e521c
|
|
||||||
subpackages:
|
|
||||||
- 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
|
|
||||||
- name: github.com/docker/go-connections
|
|
||||||
version: a2afab9802043837035592f1c24827fb70766de9
|
|
||||||
subpackages:
|
|
||||||
- nat
|
|
||||||
- name: github.com/docker/go-units
|
|
||||||
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
|
||||||
- name: github.com/fsouza/go-dockerclient
|
|
||||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
|
||||||
- name: github.com/gizak/termui
|
|
||||||
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
|
|
||||||
repo: https://github.com/bcicen/termui
|
|
||||||
vcs: git
|
|
||||||
- name: github.com/godbus/dbus
|
|
||||||
version: c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f
|
|
||||||
- name: github.com/golang/protobuf
|
|
||||||
version: f7137ae6b19afbfd61a94b746fda3b3fe0491874
|
|
||||||
subpackages:
|
|
||||||
- proto
|
|
||||||
- name: github.com/hashicorp/go-cleanhttp
|
|
||||||
version: 3573b8b52aa7b37b9358d966a898feb387f62437
|
|
||||||
- name: github.com/jgautheron/codename-generator
|
|
||||||
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
|
|
||||||
- name: github.com/maruel/panicparse
|
|
||||||
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
|
|
||||||
subpackages:
|
|
||||||
- stack
|
|
||||||
- name: github.com/mattn/go-runewidth
|
|
||||||
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
|
||||||
- name: github.com/Microsoft/go-winio
|
|
||||||
version: fff283ad5116362ca252298cfc9b95828956d85d
|
|
||||||
- name: github.com/mitchellh/go-wordwrap
|
|
||||||
version: ad45545899c7b13c020ea92b2072220eefad42b8
|
|
||||||
- name: github.com/nsf/termbox-go
|
|
||||||
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
|
|
||||||
- name: github.com/nu7hatch/gouuid
|
|
||||||
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
|
|
||||||
- name: github.com/Nvveen/Gotty
|
|
||||||
version: cd527374f1e5bff4938207604a14f2e38a9cf512
|
|
||||||
- name: github.com/op/go-logging
|
|
||||||
version: b2cb9fa56473e98db8caba80237377e83fe44db5
|
|
||||||
- name: github.com/opencontainers/runc
|
|
||||||
version: baf6536d6259209c3edfa2b22237af82942d3dfa
|
|
||||||
subpackages:
|
|
||||||
- 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
|
|
||||||
- name: github.com/seccomp/libseccomp-golang
|
|
||||||
version: 1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1
|
|
||||||
- name: github.com/Sirupsen/logrus
|
|
||||||
version: 26709e2714106fb8ad40b773b711ebce25b78914
|
|
||||||
- name: github.com/syndtr/gocapability
|
|
||||||
version: 2c00daeb6c3b45114c80ac44119e7b8801fdd852
|
|
||||||
subpackages:
|
|
||||||
- capability
|
|
||||||
- name: github.com/vishvananda/netlink
|
|
||||||
version: 1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270
|
|
||||||
subpackages:
|
|
||||||
- nl
|
|
||||||
- name: golang.org/x/net
|
|
||||||
version: a6577fac2d73be281a500b310739095313165611
|
|
||||||
subpackages:
|
|
||||||
- context
|
|
||||||
- context/ctxhttp
|
|
||||||
- name: golang.org/x/sys
|
|
||||||
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
|
|
||||||
subpackages:
|
|
||||||
- unix
|
|
||||||
- windows
|
|
||||||
testImports: []
|
|
||||||
18
glide.yaml
18
glide.yaml
@@ -1,18 +0,0 @@
|
|||||||
package: github.com/bcicen/ctop
|
|
||||||
import:
|
|
||||||
- package: github.com/c9s/goprocinfo/linux
|
|
||||||
- package: github.com/docker/docker
|
|
||||||
version: ^17.5.0-ce-rc3
|
|
||||||
- package: github.com/opencontainers/runc
|
|
||||||
version: 0.1.1
|
|
||||||
- package: github.com/fsouza/go-dockerclient
|
|
||||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
|
||||||
- package: github.com/gizak/termui
|
|
||||||
version: barchart-numfmt
|
|
||||||
repo: https://github.com/bcicen/termui
|
|
||||||
vcs: git
|
|
||||||
- package: github.com/jgautheron/codename-generator
|
|
||||||
- package: github.com/nu7hatch/gouuid
|
|
||||||
- package: github.com/c9s/goprocinfo/linux
|
|
||||||
- package: github.com/op/go-logging
|
|
||||||
version: ^1.0.0
|
|
||||||
66
grid.go
66
grid.go
@@ -2,8 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/cwidgets/single"
|
||||||
"github.com/bcicen/ctop/cwidgets/expanded"
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +17,7 @@ func RedrawRows(clr bool) {
|
|||||||
header.SetFilter(config.GetVal("filterStr"))
|
header.SetFilter(config.GetVal("filterStr"))
|
||||||
y += header.Height()
|
y += header.Height()
|
||||||
}
|
}
|
||||||
|
|
||||||
cGrid.SetY(y)
|
cGrid.SetY(y)
|
||||||
|
|
||||||
for _, c := range cursor.filtered {
|
for _, c := range cursor.filtered {
|
||||||
@@ -33,14 +33,20 @@ func RedrawRows(clr bool) {
|
|||||||
}
|
}
|
||||||
cGrid.Align()
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandView(c *container.Container) {
|
func SingleView() MenuFn {
|
||||||
|
c := cursor.Selected()
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
ex := expanded.NewExpanded(c.Id)
|
ex := single.NewSingle(c.Id)
|
||||||
c.SetUpdater(ex)
|
c.SetUpdater(ex)
|
||||||
|
|
||||||
ex.Align()
|
ex.Align()
|
||||||
@@ -59,6 +65,7 @@ func ExpandView(c *container.Container) {
|
|||||||
|
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
c.SetUpdater(c.Widgets)
|
c.SetUpdater(c.Widgets)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RefreshDisplay() {
|
func RefreshDisplay() {
|
||||||
@@ -70,14 +77,14 @@ func RefreshDisplay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Display() bool {
|
func Display() bool {
|
||||||
var menu func()
|
var menu MenuFn
|
||||||
var expand bool
|
|
||||||
|
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
ui.DefaultEvtStream.Hook(logEvent)
|
ui.DefaultEvtStream.Hook(logEvent)
|
||||||
|
|
||||||
// initial draw
|
// initial draw
|
||||||
header.Align()
|
header.Align()
|
||||||
|
status.Align()
|
||||||
cursor.RefreshContainers()
|
cursor.RefreshContainers()
|
||||||
RedrawRows(true)
|
RedrawRows(true)
|
||||||
|
|
||||||
@@ -94,7 +101,15 @@ func Display() bool {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
expand = true
|
menu = ContainerMenu
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/l", func(ui.Event) {
|
||||||
|
menu = LogMenu
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/o", func(ui.Event) {
|
||||||
|
menu = SingleView
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
||||||
@@ -119,13 +134,26 @@ func Display() bool {
|
|||||||
menu = SortMenu
|
menu = SortMenu
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
|
ui.Handle("/sys/kbd/S", func(ui.Event) {
|
||||||
|
path, err := config.Write()
|
||||||
|
if err == nil {
|
||||||
|
log.Statusf("wrote config to %s", path)
|
||||||
|
} else {
|
||||||
|
log.StatusErr(err)
|
||||||
|
}
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
|
||||||
ui.Handle("/timer/1s", func(e ui.Event) {
|
ui.Handle("/timer/1s", func(e ui.Event) {
|
||||||
|
if log.StatusQueued() {
|
||||||
|
ui.StopLoop()
|
||||||
|
}
|
||||||
RefreshDisplay()
|
RefreshDisplay()
|
||||||
})
|
})
|
||||||
|
|
||||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
header.Align()
|
header.Align()
|
||||||
|
status.Align()
|
||||||
cursor.ScrollPage()
|
cursor.ScrollPage()
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
||||||
@@ -133,16 +161,24 @@ func Display() bool {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
if menu != nil {
|
|
||||||
menu()
|
if log.StatusQueued() {
|
||||||
return false
|
for sm := range log.FlushStatus() {
|
||||||
}
|
if sm.IsError {
|
||||||
if expand {
|
status.ShowErr(sm.Text)
|
||||||
c := cursor.Selected()
|
} else {
|
||||||
if c != nil {
|
status.Show(sm.Text)
|
||||||
ExpandView(c)
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if menu != nil {
|
||||||
|
for menu != nil {
|
||||||
|
menu = menu()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
60
install.sh
Executable file
60
install.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# a simple install script for ctop
|
||||||
|
|
||||||
|
KERNEL=$(uname -s)
|
||||||
|
|
||||||
|
function output() { echo -e "\033[32mctop-install\033[0m $@"; }
|
||||||
|
|
||||||
|
# extract github download url matching pattern
|
||||||
|
function extract_url() {
|
||||||
|
match=$1; shift
|
||||||
|
echo "$@" | while read line; do
|
||||||
|
case $line in
|
||||||
|
*browser_download_url*${match}*)
|
||||||
|
url=$(echo $line | sed -e 's/^.*"browser_download_url":[ ]*"//' -e 's/".*//;s/\ //g')
|
||||||
|
echo $url
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
case $KERNEL in
|
||||||
|
Linux) MATCH_BUILD="linux-amd64" ;;
|
||||||
|
Darwin) MATCH_BUILD="darwin-amd64" ;;
|
||||||
|
*)
|
||||||
|
echo "platform not supported by this install script"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
TMP=$(mktemp -d "${TMPDIR:-/tmp}/ctop.XXXXX")
|
||||||
|
cd ${TMP}
|
||||||
|
|
||||||
|
output "fetching latest release info"
|
||||||
|
resp=$(curl -s https://api.github.com/repos/bcicen/ctop/releases/latest)
|
||||||
|
|
||||||
|
output "fetching release checksums"
|
||||||
|
checksum_url=$(extract_url sha256sums.txt "$resp")
|
||||||
|
wget -q $checksum_url -O sha256sums.txt
|
||||||
|
|
||||||
|
# skip if latest already installed
|
||||||
|
cur_ctop=$(which ctop 2> /dev/null)
|
||||||
|
if [[ -n "$cur_ctop" ]]; then
|
||||||
|
cur_sum=$(sha256sum $cur_ctop | sed 's/ .*//')
|
||||||
|
(grep -q $cur_sum sha256sums.txt) && {
|
||||||
|
output "already up-to-date"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
output "fetching latest ctop"
|
||||||
|
url=$(extract_url $MATCH_BUILD "$resp")
|
||||||
|
wget -q --show-progress $url
|
||||||
|
(sha256sum -c --quiet --ignore-missing sha256sums.txt) || exit 1
|
||||||
|
|
||||||
|
output "installing to /usr/local/bin"
|
||||||
|
chmod +x ctop-*
|
||||||
|
sudo mv ctop-* /usr/local/bin/ctop
|
||||||
|
|
||||||
|
output "done!"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -20,11 +21,36 @@ var (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type statusMsg struct {
|
||||||
|
Text string
|
||||||
|
IsError bool
|
||||||
|
}
|
||||||
|
|
||||||
type CTopLogger struct {
|
type CTopLogger struct {
|
||||||
*logging.Logger
|
*logging.Logger
|
||||||
backend *logging.MemoryBackend
|
backend *logging.MemoryBackend
|
||||||
|
sLog []statusMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CTopLogger) FlushStatus() chan statusMsg {
|
||||||
|
ch := make(chan statusMsg)
|
||||||
|
go func() {
|
||||||
|
for _, sm := range c.sLog {
|
||||||
|
ch <- sm
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
c.sLog = []statusMsg{}
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CTopLogger) StatusQueued() bool { return len(c.sLog) > 0 }
|
||||||
|
func (c *CTopLogger) Status(s string) { c.addStatus(statusMsg{s, false}) }
|
||||||
|
func (c *CTopLogger) StatusErr(err error) { c.addStatus(statusMsg{err.Error(), true}) }
|
||||||
|
func (c *CTopLogger) addStatus(sm statusMsg) { c.sLog = append(c.sLog, sm) }
|
||||||
|
|
||||||
|
func (c *CTopLogger) Statusf(s string, a ...interface{}) { c.Status(fmt.Sprintf(s, a...)) }
|
||||||
|
|
||||||
func Init() *CTopLogger {
|
func Init() *CTopLogger {
|
||||||
if Log == nil {
|
if Log == nil {
|
||||||
logging.SetFormatter(format) // setup default formatter
|
logging.SetFormatter(format) // setup default formatter
|
||||||
@@ -32,6 +58,7 @@ func Init() *CTopLogger {
|
|||||||
Log = &CTopLogger{
|
Log = &CTopLogger{
|
||||||
logging.MustGetLogger("ctop"),
|
logging.MustGetLogger("ctop"),
|
||||||
logging.NewMemoryBackend(size),
|
logging.NewMemoryBackend(size),
|
||||||
|
[]statusMsg{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugMode() {
|
if debugMode() {
|
||||||
|
|||||||
36
main.go
36
main.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/connector"
|
"github.com/bcicen/ctop/connector"
|
||||||
@@ -16,29 +17,34 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
build = "none"
|
build = "none"
|
||||||
version = "dev-build"
|
version = "dev-build"
|
||||||
|
goVersion = runtime.Version()
|
||||||
|
|
||||||
log *logging.CTopLogger
|
log *logging.CTopLogger
|
||||||
cursor *GridCursor
|
cursor *GridCursor
|
||||||
cGrid *compact.CompactGrid
|
cGrid *compact.CompactGrid
|
||||||
header *widgets.CTopHeader
|
header *widgets.CTopHeader
|
||||||
|
status *widgets.StatusLine
|
||||||
|
|
||||||
versionStr = fmt.Sprintf("ctop version %v, build %v", version, build)
|
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer panicExit()
|
defer panicExit()
|
||||||
|
|
||||||
// parse command line arguments
|
// parse command line arguments
|
||||||
var versionFlag = flag.Bool("v", false, "output version information and exit")
|
var (
|
||||||
var helpFlag = flag.Bool("h", false, "display this help dialog")
|
versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||||
var filterFlag = flag.String("f", "", "filter containers")
|
helpFlag = flag.Bool("h", false, "display this help dialog")
|
||||||
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
filterFlag = flag.String("f", "", "filter containers")
|
||||||
var sortFieldFlag = flag.String("s", "", "select container sort field")
|
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
||||||
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
sortFieldFlag = flag.String("s", "", "select container sort field")
|
||||||
var invertFlag = flag.Bool("i", false, "invert default colors")
|
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
||||||
var connectorFlag = flag.String("connector", "docker", "container connector to use")
|
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")
|
||||||
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *versionFlag {
|
if *versionFlag {
|
||||||
@@ -54,8 +60,9 @@ func main() {
|
|||||||
// init logger
|
// init logger
|
||||||
log = logging.Init()
|
log = logging.Init()
|
||||||
|
|
||||||
// init global config
|
// init global config and read config file if exists
|
||||||
config.Init()
|
config.Init()
|
||||||
|
config.Read()
|
||||||
|
|
||||||
// override default config values with command line flags
|
// override default config values with command line flags
|
||||||
if *filterFlag != "" {
|
if *filterFlag != "" {
|
||||||
@@ -75,6 +82,10 @@ func main() {
|
|||||||
config.Toggle("sortReversed")
|
config.Toggle("sortReversed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *scaleCpu {
|
||||||
|
config.Toggle("scaleCpu")
|
||||||
|
}
|
||||||
|
|
||||||
// init ui
|
// init ui
|
||||||
if *invertFlag {
|
if *invertFlag {
|
||||||
InvertColorMap()
|
InvertColorMap()
|
||||||
@@ -93,6 +104,7 @@ func main() {
|
|||||||
cursor = &GridCursor{cSource: conn}
|
cursor = &GridCursor{cSource: conn}
|
||||||
cGrid = compact.NewCompactGrid()
|
cGrid = compact.NewCompactGrid()
|
||||||
header = widgets.NewCTopHeader()
|
header = widgets.NewCTopHeader()
|
||||||
|
status = widgets.NewStatusLine()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
exit := Display()
|
exit := Display()
|
||||||
|
|||||||
210
menus.go
210
menus.go
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/widgets"
|
"github.com/bcicen/ctop/widgets"
|
||||||
@@ -8,17 +11,25 @@ import (
|
|||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MenuFn executes a menu window, returning the next menu or nil
|
||||||
|
type MenuFn func() MenuFn
|
||||||
|
|
||||||
var helpDialog = []menu.Item{
|
var helpDialog = []menu.Item{
|
||||||
menu.Item{"[a] - toggle display of all containers", ""},
|
{"<enter> - open container menu", ""},
|
||||||
menu.Item{"[f] - filter displayed containers", ""},
|
{"", ""},
|
||||||
menu.Item{"[h] - open this help dialog", ""},
|
{"[a] - toggle display of all containers", ""},
|
||||||
menu.Item{"[H] - toggle ctop header", ""},
|
{"[f] - filter displayed containers", ""},
|
||||||
menu.Item{"[s] - select container sort field", ""},
|
{"[h] - open this help dialog", ""},
|
||||||
menu.Item{"[r] - reverse container sort order", ""},
|
{"[H] - toggle ctop header", ""},
|
||||||
menu.Item{"[q] - exit ctop", ""},
|
{"[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] - exit ctop", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
func HelpMenu() {
|
func HelpMenu() MenuFn {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
@@ -31,9 +42,10 @@ func HelpMenu() {
|
|||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilterMenu() {
|
func FilterMenu() MenuFn {
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
@@ -63,9 +75,10 @@ func FilterMenu() {
|
|||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortMenu() {
|
func SortMenu() MenuFn {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
@@ -93,4 +106,181 @@ func SortMenu() {
|
|||||||
|
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ContainerMenu() MenuFn {
|
||||||
|
c := cursor.Selected()
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
|
m := menu.NewMenu()
|
||||||
|
m.Selectable = true
|
||||||
|
m.BorderLabel = "Menu"
|
||||||
|
|
||||||
|
items := []menu.Item{
|
||||||
|
menu.Item{Val: "single", Label: "single view"},
|
||||||
|
menu.Item{Val: "logs", Label: "log view"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Meta["state"] == "running" {
|
||||||
|
items = append(items, menu.Item{Val: "stop", Label: "stop"})
|
||||||
|
}
|
||||||
|
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: "cancel", Label: "cancel"})
|
||||||
|
|
||||||
|
m.AddItems(items...)
|
||||||
|
ui.Render(m)
|
||||||
|
|
||||||
|
var nextMenu MenuFn
|
||||||
|
HandleKeys("up", m.Up)
|
||||||
|
HandleKeys("down", m.Down)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Loop()
|
||||||
|
return nextMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogMenu() MenuFn {
|
||||||
|
|
||||||
|
c := cursor.Selected()
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
|
logs, quit := logReader(c)
|
||||||
|
m := widgets.NewTextView(logs)
|
||||||
|
m.BorderLabel = "Logs"
|
||||||
|
ui.Render(m)
|
||||||
|
|
||||||
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
|
m.Resize()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/t", func(ui.Event) {
|
||||||
|
m.Toggle()
|
||||||
|
})
|
||||||
|
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||||
|
quit <- true
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Loop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a confirmation dialog with a given description string and
|
||||||
|
// func to perform if confirmed
|
||||||
|
func Confirm(txt string, fn func()) MenuFn {
|
||||||
|
menu := func() MenuFn {
|
||||||
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
|
m := menu.NewMenu()
|
||||||
|
m.Selectable = true
|
||||||
|
m.BorderLabel = "Confirm"
|
||||||
|
m.SubText = txt
|
||||||
|
|
||||||
|
items := []menu.Item{
|
||||||
|
menu.Item{Val: "cancel", Label: "[c]ancel"},
|
||||||
|
menu.Item{Val: "yes", Label: "[y]es"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var response bool
|
||||||
|
|
||||||
|
m.AddItems(items...)
|
||||||
|
ui.Render(m)
|
||||||
|
|
||||||
|
yes := func() {
|
||||||
|
response = true
|
||||||
|
ui.StopLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
no := func() {
|
||||||
|
response = false
|
||||||
|
ui.StopLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleKeys("up", m.Up)
|
||||||
|
HandleKeys("down", m.Down)
|
||||||
|
HandleKeys("exit", no)
|
||||||
|
ui.Handle("/sys/kbd/c", func(ui.Event) { no() })
|
||||||
|
ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })
|
||||||
|
|
||||||
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
|
switch m.SelectedItem().Val {
|
||||||
|
case "cancel":
|
||||||
|
no()
|
||||||
|
case "yes":
|
||||||
|
yes()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.Loop()
|
||||||
|
if response {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
|
type toggleLog struct {
|
||||||
|
timestamp time.Time
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *toggleLog) Toggle(on bool) string {
|
||||||
|
if on {
|
||||||
|
return fmt.Sprintf("%s %s", t.timestamp.Format("2006-01-02T15:04:05.999Z07:00"), t.message)
|
||||||
|
}
|
||||||
|
return t.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func logReader(container *container.Container) (logs chan widgets.ToggleText, quit chan bool) {
|
||||||
|
|
||||||
|
logCollector := container.Logs()
|
||||||
|
stream := logCollector.Stream()
|
||||||
|
logs = make(chan widgets.ToggleText)
|
||||||
|
quit = make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case log := <-stream:
|
||||||
|
logs <- &toggleLog{timestamp: log.Timestamp, message: log.Message}
|
||||||
|
case <-quit:
|
||||||
|
logCollector.Stop()
|
||||||
|
close(logs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmTxt(a, n string) string { return fmt.Sprintf("%s container %s?", a, n) }
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
package metrics
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
CPUUtil int
|
CPUUtil int
|
||||||
@@ -24,10 +31,3 @@ func NewMetrics() Metrics {
|
|||||||
Pids: -1,
|
Pids: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Collector interface {
|
|
||||||
Stream() chan Metrics
|
|
||||||
Running() bool
|
|
||||||
Start()
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
@@ -17,8 +17,8 @@ type CTopHeader struct {
|
|||||||
func NewCTopHeader() *CTopHeader {
|
func NewCTopHeader() *CTopHeader {
|
||||||
return &CTopHeader{
|
return &CTopHeader{
|
||||||
Time: headerPar(2, timeStr()),
|
Time: headerPar(2, timeStr()),
|
||||||
Count: headerPar(27, "-"),
|
Count: headerPar(24, "-"),
|
||||||
Filter: headerPar(47, ""),
|
Filter: headerPar(40, ""),
|
||||||
bg: headerBg(),
|
bg: headerBg(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ type Padding [2]int // x,y padding
|
|||||||
|
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
ui.Block
|
ui.Block
|
||||||
SortItems bool // enable automatic sorting of menu items
|
SortItems bool // enable automatic sorting of menu items
|
||||||
|
SubText string // optional text to display before items
|
||||||
TextFgColor ui.Attribute
|
TextFgColor ui.Attribute
|
||||||
TextBgColor ui.Attribute
|
TextBgColor ui.Attribute
|
||||||
Selectable bool
|
Selectable bool
|
||||||
@@ -82,9 +83,19 @@ func (m *Menu) Buffer() ui.Buffer {
|
|||||||
var cell ui.Cell
|
var cell ui.Cell
|
||||||
buf := m.Block.Buffer()
|
buf := m.Block.Buffer()
|
||||||
|
|
||||||
|
y := m.Y + m.padding[1]
|
||||||
|
|
||||||
|
if m.SubText != "" {
|
||||||
|
x := m.X + m.padding[0]
|
||||||
|
for i, ch := range m.SubText {
|
||||||
|
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
||||||
|
buf.Set(x+i, y, cell)
|
||||||
|
}
|
||||||
|
y += 2
|
||||||
|
}
|
||||||
|
|
||||||
for n, item := range m.items {
|
for n, item := range m.items {
|
||||||
x := m.X + m.padding[0]
|
x := m.X + m.padding[0]
|
||||||
y := m.Y + m.padding[1]
|
|
||||||
for _, ch := range item.Text() {
|
for _, ch := range item.Text() {
|
||||||
// invert bg/fg colors on currently selected row
|
// invert bg/fg colors on currently selected row
|
||||||
if m.Selectable && n == m.cursorPos {
|
if m.Selectable && n == m.cursorPos {
|
||||||
@@ -118,14 +129,22 @@ func (m *Menu) Down() {
|
|||||||
func (m *Menu) calcSize() {
|
func (m *Menu) calcSize() {
|
||||||
m.Width = 7 // minimum width
|
m.Width = 7 // minimum width
|
||||||
|
|
||||||
items := m.items
|
var height int
|
||||||
for _, i := range m.items {
|
for _, i := range m.items {
|
||||||
s := i.Text()
|
s := i.Text()
|
||||||
if len(s) > m.Width {
|
if len(s) > m.Width {
|
||||||
m.Width = len(s)
|
m.Width = len(s)
|
||||||
}
|
}
|
||||||
|
height++
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.SubText != "" {
|
||||||
|
if len(m.SubText) > m.Width {
|
||||||
|
m.Width = len(m.SubText)
|
||||||
|
}
|
||||||
|
height += 2
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Width += (m.padding[0] * 2)
|
m.Width += (m.padding[0] * 2)
|
||||||
m.Height = len(items) + (m.padding[1] * 2)
|
m.Height = height + (m.padding[1] * 2)
|
||||||
}
|
}
|
||||||
|
|||||||
87
widgets/status.go
Normal file
87
widgets/status.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
statusHeight = 1
|
||||||
|
statusIter = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusLine struct {
|
||||||
|
Message *ui.Par
|
||||||
|
bg *ui.Par
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatusLine() *StatusLine {
|
||||||
|
p := ui.NewPar("")
|
||||||
|
p.X = 2
|
||||||
|
p.Border = false
|
||||||
|
p.Height = statusHeight
|
||||||
|
p.Bg = ui.ThemeAttr("header.bg")
|
||||||
|
p.TextFgColor = ui.ThemeAttr("header.fg")
|
||||||
|
p.TextBgColor = ui.ThemeAttr("header.bg")
|
||||||
|
return &StatusLine{
|
||||||
|
Message: p,
|
||||||
|
bg: statusBg(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *StatusLine) Display() {
|
||||||
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
|
iter := statusIter
|
||||||
|
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
ui.Handle("/timer/1s", func(ui.Event) {
|
||||||
|
iter--
|
||||||
|
if iter <= 0 {
|
||||||
|
ui.StopLoop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.Render(sl)
|
||||||
|
ui.Loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// change given message on the status line
|
||||||
|
func (sl *StatusLine) Show(s string) {
|
||||||
|
sl.Message.TextFgColor = ui.ThemeAttr("header.fg")
|
||||||
|
sl.Message.Text = s
|
||||||
|
sl.Display()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *StatusLine) ShowErr(s string) {
|
||||||
|
sl.Message.TextFgColor = ui.ThemeAttr("status.danger")
|
||||||
|
sl.Message.Text = s
|
||||||
|
sl.Display()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *StatusLine) Buffer() ui.Buffer {
|
||||||
|
buf := ui.NewBuffer()
|
||||||
|
buf.Merge(sl.bg.Buffer())
|
||||||
|
buf.Merge(sl.Message.Buffer())
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *StatusLine) Align() {
|
||||||
|
sl.bg.SetWidth(ui.TermWidth() - 1)
|
||||||
|
sl.Message.SetWidth(ui.TermWidth() - 2)
|
||||||
|
|
||||||
|
sl.bg.Y = ui.TermHeight() - 1
|
||||||
|
sl.Message.Y = ui.TermHeight() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *StatusLine) Height() int { return statusHeight }
|
||||||
|
|
||||||
|
func statusBg() *ui.Par {
|
||||||
|
bg := ui.NewPar("")
|
||||||
|
bg.X = 1
|
||||||
|
bg.Height = statusHeight
|
||||||
|
bg.Border = false
|
||||||
|
bg.Bg = ui.ThemeAttr("header.bg")
|
||||||
|
return bg
|
||||||
|
}
|
||||||
124
widgets/view.go
Normal file
124
widgets/view.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
ui "github.com/gizak/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToggleText interface {
|
||||||
|
// returns text for toggle on/off
|
||||||
|
Toggle(on bool) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextView struct {
|
||||||
|
ui.Block
|
||||||
|
inputStream <-chan ToggleText
|
||||||
|
render chan bool
|
||||||
|
toggleState bool
|
||||||
|
Text []ToggleText // all the text
|
||||||
|
TextOut []string // text to be displayed
|
||||||
|
TextFgColor ui.Attribute
|
||||||
|
TextBgColor ui.Attribute
|
||||||
|
padding Padding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextView(lines <-chan ToggleText) *TextView {
|
||||||
|
t := &TextView{
|
||||||
|
Block: *ui.NewBlock(),
|
||||||
|
inputStream: lines,
|
||||||
|
render: make(chan bool),
|
||||||
|
Text: []ToggleText{},
|
||||||
|
TextOut: []string{},
|
||||||
|
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||||
|
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||||
|
padding: Padding{4, 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||||
|
t.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||||
|
t.Height = ui.TermHeight()
|
||||||
|
t.Width = ui.TermWidth()
|
||||||
|
|
||||||
|
t.readInputLoop()
|
||||||
|
t.renderLoop()
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjusts text inside this view according to the window size. No need to call ui.Render(...)
|
||||||
|
// after calling this method, it is called automatically
|
||||||
|
func (t *TextView) Resize() {
|
||||||
|
ui.Clear()
|
||||||
|
t.Height = ui.TermHeight()
|
||||||
|
t.Width = ui.TermWidth()
|
||||||
|
t.render <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles text inside this view. No need to call ui.Render(...) after calling this method,
|
||||||
|
// it is called automatically
|
||||||
|
func (t *TextView) Toggle() {
|
||||||
|
t.toggleState = !t.toggleState
|
||||||
|
t.render <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextView) Buffer() ui.Buffer {
|
||||||
|
var cell ui.Cell
|
||||||
|
buf := t.Block.Buffer()
|
||||||
|
|
||||||
|
x := t.Block.X + t.padding[0]
|
||||||
|
y := t.Block.Y + t.padding[1]
|
||||||
|
|
||||||
|
for _, line := range t.TextOut {
|
||||||
|
for _, ch := range line {
|
||||||
|
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
|
||||||
|
buf.Set(x, y, cell)
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
x = t.Block.X + t.padding[0]
|
||||||
|
y++
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextView) renderLoop() {
|
||||||
|
go func() {
|
||||||
|
for range t.render {
|
||||||
|
maxWidth := t.Width - (t.padding[0] * 2)
|
||||||
|
height := t.Height - (t.padding[1] * 2)
|
||||||
|
t.TextOut = []string{}
|
||||||
|
for i := len(t.Text) - 1; i >= 0; i-- {
|
||||||
|
lines := splitLine(t.Text[i].Toggle(t.toggleState), maxWidth)
|
||||||
|
t.TextOut = append(lines, t.TextOut...)
|
||||||
|
if len(t.TextOut) > height {
|
||||||
|
t.TextOut = t.TextOut[:height]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.Render(t)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TextView) readInputLoop() {
|
||||||
|
go func() {
|
||||||
|
for line := range t.inputStream {
|
||||||
|
t.Text = append(t.Text, line)
|
||||||
|
t.render <- true
|
||||||
|
}
|
||||||
|
close(t.render)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitLine(line string, lineSize int) []string {
|
||||||
|
if line == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
for {
|
||||||
|
if len(line) <= lineSize {
|
||||||
|
lines = append(lines, line)
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
lines = append(lines, line[:lineSize])
|
||||||
|
line = line[lineSize:]
|
||||||
|
}
|
||||||
|
}
|
||||||
35
widgets/view_test.go
Normal file
35
widgets/view_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSplitEmptyLine(t *testing.T) {
|
||||||
|
|
||||||
|
result := splitLine("", 5)
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitLineShorterThanLimit(t *testing.T) {
|
||||||
|
|
||||||
|
result := splitLine("hello", 7)
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitLineLongerThanLimit(t *testing.T) {
|
||||||
|
|
||||||
|
result := splitLine("hello", 3)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitLineSameAsLimit(t *testing.T) {
|
||||||
|
|
||||||
|
result := splitLine("hello", 5)
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user