mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
52 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 |
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
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM quay.io/vektorcloud/go:1.8
|
FROM quay.io/vektorcloud/go:1.9
|
||||||
|
|
||||||
RUN apk add --no-cache make
|
RUN apk add --no-cache make
|
||||||
|
|
||||||
COPY glide.* /go/src/github.com/bcicen/ctop/
|
COPY Gopkg.* /go/src/github.com/bcicen/ctop/
|
||||||
WORKDIR /go/src/github.com/bcicen/ctop/
|
WORKDIR /go/src/github.com/bcicen/ctop/
|
||||||
RUN glide install
|
RUN dep ensure -vendor-only
|
||||||
|
|
||||||
COPY . /go/src/github.com/bcicen/ctop
|
COPY . /go/src/github.com/bcicen/ctop
|
||||||
RUN make build && \
|
RUN make build && \
|
||||||
|
|||||||
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"
|
||||||
12
Makefile
12
Makefile
@@ -5,29 +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 -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.0/ctop-0.6.0-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.0/ctop-0.6.0-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,6 +1,7 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/models"
|
"github.com/bcicen/ctop/models"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,7 @@ type Docker struct {
|
|||||||
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 {
|
||||||
@@ -22,6 +24,7 @@ func NewDocker(client *api.Client, id string) *Docker {
|
|||||||
Metrics: models.Metrics{},
|
Metrics: models.Metrics{},
|
||||||
id: id,
|
id: id,
|
||||||
client: client,
|
client: client,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ func (c *Docker) Stream() chan models.Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Logs() LogCollector {
|
func (c *Docker) Logs() LogCollector {
|
||||||
return &DockerLogs{c.id, c.client, make(chan bool)}
|
return NewDockerLogs(c.id, c.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop collector
|
// Stop collector
|
||||||
@@ -82,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
|
||||||
|
|
||||||
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ type DockerLogs struct {
|
|||||||
done chan bool
|
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 {
|
func (l *DockerLogs) Stream() chan models.Log {
|
||||||
r, w := io.Pipe()
|
r, w := io.Pipe()
|
||||||
logCh := make(chan models.Log)
|
logCh := make(chan models.Log)
|
||||||
@@ -50,6 +58,7 @@ func (l *DockerLogs) Stream() chan models.Log {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error reading container logs: %s", err)
|
log.Errorf("error reading container logs: %s", err)
|
||||||
}
|
}
|
||||||
|
log.Infof("log reader stopped for container: %s", l.id)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -59,6 +68,7 @@ func (l *DockerLogs) Stream() chan models.Log {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
log.Infof("log reader started for container: %s", l.id)
|
||||||
return logCh
|
return logCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package collector
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/models"
|
"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"
|
||||||
@@ -21,6 +22,7 @@ type Runc struct {
|
|||||||
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 {
|
||||||
@@ -29,6 +31,7 @@ func NewRunc(libc libcontainer.Container) *Runc {
|
|||||||
id: libc.ID(),
|
id: libc.ID(),
|
||||||
libc: libc,
|
libc: libc,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -90,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
|
||||||
|
|
||||||
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
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":
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"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"
|
||||||
@@ -21,9 +22,10 @@ type Container struct {
|
|||||||
Display bool // display this container in compact view
|
Display bool // display this container in compact view
|
||||||
updater cwidgets.WidgetUpdater
|
updater cwidgets.WidgetUpdater
|
||||||
collector collector.Collector
|
collector collector.Collector
|
||||||
|
manager manager.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(id string, collector collector.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: models.NewMetrics(),
|
Metrics: models.NewMetrics(),
|
||||||
@@ -32,6 +34,7 @@ func New(id string, collector collector.Collector) *Container {
|
|||||||
Widgets: widgets,
|
Widgets: widgets,
|
||||||
updater: widgets,
|
updater: widgets,
|
||||||
collector: collector,
|
collector: collector,
|
||||||
|
manager: manager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,3 +88,32 @@ func (c *Container) Read(stream chan models.Metrics) {
|
|||||||
}()
|
}()
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +56,8 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ func (row *Compact) SetMetrics(m models.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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
@@ -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 models.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 models.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"
|
||||||
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
|
|
||||||
64
grid.go
64
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 SingleView() MenuFn {
|
||||||
|
c := cursor.Selected()
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandView(c *container.Container) {
|
|
||||||
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 log.StatusQueued() {
|
||||||
|
for sm := range log.FlushStatus() {
|
||||||
|
if sm.IsError {
|
||||||
|
status.ShowErr(sm.Text)
|
||||||
|
} else {
|
||||||
|
status.Show(sm.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if menu != nil {
|
if menu != nil {
|
||||||
menu()
|
for menu != nil {
|
||||||
return false
|
menu = menu()
|
||||||
}
|
|
||||||
if expand {
|
|
||||||
c := cursor.Selected()
|
|
||||||
if c != nil {
|
|
||||||
ExpandView(c)
|
|
||||||
}
|
}
|
||||||
return false
|
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() {
|
||||||
|
|||||||
28
main.go
28
main.go
@@ -25,6 +25,7 @@ var (
|
|||||||
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 %v", version, build, goVersion)
|
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||||
)
|
)
|
||||||
@@ -33,14 +34,17 @@ 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 {
|
||||||
@@ -56,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 != "" {
|
||||||
@@ -77,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()
|
||||||
@@ -95,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) }
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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