Compare commits

...

52 Commits
v0.6.1 ... v0.7

Author SHA1 Message Date
Bradley Cicenas
233259be40 v0.7 2018-01-11 15:32:34 -03:00
Bradley Cicenas
107def9ccc add status err messages to start/stop/remove 2018-01-11 15:27:30 -03:00
Bradley Cicenas
d46ce783c2 add statusline widget, status messages to logging 2018-01-11 15:19:01 -03:00
Bradley Cicenas
d743472b16 add support for config file, keybinding for exporting active config 2018-01-11 13:15:18 -03:00
Bradley Cicenas
d785b263f4 add scale-cpu switch 2018-01-11 12:19:00 -03:00
Bradley Cicenas
6d37a4e333 enable remove action for "created" state containers 2018-01-11 11:40:21 -03:00
Bradley Cicenas
eb49e51ffb prevent recursive/nested running menus, handle all within main Display() loop 2018-01-11 11:23:28 -03:00
Bradley Cicenas
734d4bfc0c add optional subtext to menu widget 2018-01-11 10:56:00 -03:00
Bradley Cicenas
0e75bbda58 add confirmation dialog for container management actions 2018-01-11 10:26:58 -03:00
Bradley Cicenas
8b5eb21ac3 add logs to container menu 2018-01-10 18:10:11 -03:00
Bradley Cicenas
c958e4c34e move keybinding for single view, add single view to container menu 2018-01-10 15:45:14 -03:00
Bradley Cicenas
ab48d830d1 import formatting 2017-12-13 09:20:14 +08:00
bradley
915579c0a4 Merge pull request #110 from HotelsDotCom/master
added option to toggle log timestamp in the log view, closes #107
2017-12-13 09:16:33 +08:00
Peter Reisinger
0a6e6f02a4 added option to toggle log timestamp in the log view, closes #107 2017-12-08 11:35:04 +00:00
Bradley Cicenas
a135f45844 add simple output fn to installer script 2017-12-08 09:37:54 +08:00
Bradley Cicenas
b39b91774d add checksum validation to installer script 2017-12-05 17:50:00 +08:00
Bradley Cicenas
2275248813 init simple installer script 2017-12-02 20:49:37 +00:00
bradley
75f4a91f11 Merge pull request #108 from HotelsDotCom/master
line wrapping in log view, closes #106
2017-12-01 21:47:05 -05:00
Peter Reisinger
d0b5c6c854 added test for text view split line 2017-12-01 15:51:55 +00:00
Peter Reisinger
bd8940ae0a line wrapping in log view, closes #106 2017-12-01 15:29:11 +00:00
Bradley Cicenas
1be64f0d11 define flag vars in block 2017-11-29 03:03:46 +00:00
Bradley Cicenas
1c8f4b3a35 add logging for log reader start/stop 2017-11-28 09:38:32 -05:00
Bradley Cicenas
3e5176a79c add NewDockerLogs constructor method 2017-11-28 09:36:28 -05:00
Bradley Cicenas
53c0f2a9df add resize handling to log view 2017-11-28 08:55:29 -05:00
Bradley Cicenas
28389aa38c gofmt 2017-11-28 08:40:43 -05:00
bradley
fb5c825cf6 Merge pull request #105 from HotelsDotCom/master
Added 'view logs binding'
2017-11-26 12:41:57 -05:00
Peter Reisinger
a0e0da1da9 Added 'view logs binding' 2017-11-25 18:30:50 +00:00
Bradley Cicenas
a826859202 gofmt 2017-11-22 09:27:38 -05:00
Bradley Cicenas
71b4a1de94 add runc manager placeholder 2017-11-22 09:26:01 -05:00
bradley
93a2e4b1ca Merge pull request #104 from HotelsDotCom/master
added container menu - closes #28
2017-11-22 08:22:37 -06:00
Peter Reisinger
436266b1a4 added container menu closes #28 2017-11-20 11:09:36 +00:00
Bradley Cicenas
19427e33a0 add checksum verify to make release 2017-11-19 15:06:29 +00:00
Bradley Cicenas
48d683be77 add checksum generation to makefile 2017-11-18 07:51:15 +00:00
bradley
e1ec264345 Merge pull request #90 from bcicen/godep
Move from Glide -> Dep
2017-09-03 11:10:59 +09:00
Bradley Cicenas
9aad2efdb0 update build docs with dep link 2017-09-03 10:53:01 +09:00
Bradley Cicenas
f6595a02c4 replace glide with godep 2017-09-03 10:33:14 +09:00
Bradley Cicenas
92ca9bf7eb remove textcol coloring 2017-08-28 10:48:13 +09:00
Bradley Cicenas
05242a83f0 refactor status widget, include health indicator 2017-08-28 10:45:14 +09:00
Bradley Cicenas
add44c0f18 add common status colors to global theme 2017-08-28 08:46:01 +09:00
Bradley Cicenas
a1ebf3f90e remove duplicate inspect, trigger refresh on "health_status" events 2017-08-28 08:29:59 +09:00
Alexandr Kozlenkov
626d50d3e9 Added health row to Info Single mode.
Change color status logic.

Highlight health status for Name column:
- starting - yellow
- healthy - green
- unhealthy - magenta

reformat

Fixed misprint

Removed unused colors of state widget.

Moved changes to another branch

Removed unused colors of state widget.

Remove swarm changes from master

Remove swarm changes from master

Remove swarm changes from master
2017-08-28 07:55:43 +09:00
bradley
eaa7ad85f8 Merge pull request #87 from mavimo/patch-1
Update version in readme
2017-08-25 14:22:04 +09:00
Marco Vito Moscaritolo
be9be0b2d1 Update version in readme
SSIA
2017-08-24 12:27:15 +02:00
bradley
f196999c67 Merge pull request #86 from kentaost/runc-root-description
update RUNC_ROOT description to adjust to runC man
2017-08-22 15:22:19 +09:00
Kenta Tada
e674ec4f33 update RUNC_ROOT description to adjust to runC man 2017-08-21 21:36:45 +09:00
Bradley Cicenas
f23805550f remove deprecated -e option 2017-08-19 14:33:56 +09:00
Bradley Cicenas
55a356bbec omit cache from memory usage stat 2017-08-11 16:44:52 +02:00
Bradley Cicenas
954aaeb06b rename Memory widget 2017-08-09 14:05:45 +02:00
Bradley Cicenas
27e272c58f rename expanded -> single view 2017-08-05 13:28:20 +02:00
Bradley Cicenas
3ed9912bcb update circleci 2017-06-29 14:02:52 +00:00
Bradley Cicenas
e0f6563a39 move circleci config to 2.0 2017-06-29 14:02:52 +00:00
Bradley Cicenas
caa64724d0 fix release dirname 2017-06-29 14:02:52 +00:00
51 changed files with 1279 additions and 295 deletions

19
.circleci/config.yml Normal file
View 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

View File

@@ -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
View 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
View 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"

View File

@@ -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)

View File

@@ -9,7 +9,7 @@ Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers: `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"

View File

@@ -1 +1 @@
0.6.1 0.7

View File

@@ -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 && \

View File

@@ -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

View File

@@ -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>

View File

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 549 KiB

4
_docs/single.md Normal file
View 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>

View File

@@ -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}

View File

@@ -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
View 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]), "/")
}

View File

@@ -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)

View File

@@ -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))
} }

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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()

View 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
}

View File

@@ -0,0 +1,7 @@
package manager
type Manager interface {
Start() error
Stop() error
Remove() error
}

19
connector/manager/mock.go Normal file
View 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
View 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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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")
} }

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -1,4 +1,4 @@
package expanded package single
import ( import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,4 @@
package expanded package single
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package expanded package single
import ( import (
"time" "time"

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
package expanded package single
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package expanded package single
import ( import (
"fmt" "fmt"

129
glide.lock generated
View File

@@ -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: []

View File

@@ -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
View File

@@ -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
View 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!"

View File

@@ -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
View File

@@ -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
View File

@@ -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) }

View File

@@ -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(),
} }
} }

View File

@@ -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
View 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
View 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
View 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))
}
}