Compare commits

...

103 Commits
v0.6.1 ... k8s

Author SHA1 Message Date
Bradley Cicenas
ae62c388bc move flags to FlagSet 2018-12-02 12:50:49 +00:00
Bradley Cicenas
523d6c7277 update for latest k8s libs 2018-12-02 12:27:47 +00:00
Bradley Cicenas
47a0d7e495 fix namespace flag help text 2018-12-02 00:46:39 +00:00
Alexandr Kozlenkov
9dec6b4c67 Added loading CPU and Mem for containers in pod 2018-12-01 19:44:34 -05:00
Alexandr Kozlenkov
7f6ff0b599 Added draft for collector metrics of k8s 2018-12-01 19:44:01 -05:00
Alexandr Kozlenkov
187adf0540 A draft of connector to kubernetse 2018-12-01 19:43:41 -05:00
Bradley Cicenas
665e8fdd06 move to go module 2018-12-01 17:50:47 +00:00
bradley
a39b7a3a3e Merge pull request #152 from barthr/master
Refactoring improvements based on linting issues
2018-10-26 09:00:02 -05:00
bartfokker
77f5e6b735 remove ignore of variable (unneeded when only index is needed) 2018-10-25 22:25:36 +02:00
bartfokker
3c83b7576b refactor string on multiple places to constant 2018-10-25 22:23:44 +02:00
bartfokker
8a0bd3cf8a remove unneeded cast 2018-10-25 22:22:28 +02:00
bartfokker
78caad2dbd depend on io.WriteCloser instead of net.Conn 2018-10-25 22:22:04 +02:00
bartfokker
8d8f1e72eb rename ConfigFile to File because config.ConfigFile stutters. Instead it's config.File 2018-10-25 22:21:08 +02:00
bartfokker
93556a1754 replace += with ++ 2018-10-25 22:17:53 +02:00
bartfokker
4d247f5272 replace unkeyed fiels with keyed fields when instantiating log struct 2018-10-25 22:17:05 +02:00
bartfokker
db3d7e8927 change strings.Index for strings.Contains 2018-10-25 22:14:00 +02:00
bartfokker
efef345665 remove unneeded fmt.Sprintf 2018-10-25 22:13:04 +02:00
bartfokker
f158fa742f simplify append operation by omitting loop 2018-10-25 22:12:46 +02:00
bartfokker
4d48245d7d improve boolean logic 2018-10-25 22:12:17 +02:00
bartfokker
6bee1b7f31 remove unneeded select for simple channel receive 2018-10-25 22:11:17 +02:00
bartfokker
7118e45f3a add vendor directory to gitignore 2018-10-25 22:04:40 +02:00
bradley
3405d19be8 Merge pull request #147 from serg-bloim/env-var
Display environment variables on single view page
2018-10-10 21:45:08 +08:00
Serhii Bilonozhko
9a185b2388 env-var 2018-10-05 17:35:22 -04:00
Bradley Cicenas
caf6fc63c1 add config toggle for full-row cursor 2018-09-17 01:33:52 +00:00
Bradley Cicenas
cf352f7c8a implement full-row cursor highlighting 2018-09-17 01:24:06 +00:00
bradley
ac5bed210f Merge pull request #143 from jphautin/add_networks_ips
add IP of networks in single view mode
2018-09-15 10:43:22 +09:00
Jean-Philippe
a72d43526f add IP of networks in single view mode 2018-09-06 21:01:16 +02:00
bradley
9eb2457aa4 Merge pull request #132 from xiechengsheng/fix-120
add support for alternative navigation
2018-06-30 08:31:44 +02:00
Bradley Cicenas
b83402b886 add TERM env var to Dockerfile 2018-06-28 11:19:07 +00:00
xiechengsheng
078564bd38 add support for alternative navigation
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
2018-06-28 16:40:26 +08:00
bradley
a2c08d312e Merge pull request #131 from xiechengsheng/add-pause-unpause
Feature: add more commands in container manager menu
2018-06-28 10:25:45 +02:00
xiechengsheng
f7a3d38d6b add more commands in container manager menu
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
2018-06-22 15:41:16 +08:00
Bradley Cicenas
a3b8585697 add requirement check to install script 2018-06-13 09:20:05 +00:00
bradley
2e526e9b86 Merge pull request #130 from felipeconti/master
Installing on /usr/local/bin as root
2018-06-13 11:04:34 +02:00
Felipe B. Conti
541fe70b78 Remove unnecessary treatment 2018-06-10 14:12:07 -03:00
Felipe Conti
c786b697bf Add function command_exists 2018-06-10 14:04:34 -03:00
Felipe Conti
aa6c00b083 Treatment to use root 2018-06-10 14:02:42 -03:00
Bradley Cicenas
17855e3d8e add container name to log view title 2018-05-10 09:53:59 +00:00
Bradley Cicenas
842809bef5 enable termbox alt input 2018-05-10 09:44:57 +00:00
Bradley Cicenas
4e567ee007 update README 2018-03-09 05:37:36 +00:00
Bradley Cicenas
56700e120b add go-winio dep 2018-03-09 05:28:11 +00:00
Bradley Cicenas
5261444265 v0.7.1 2018-03-09 05:19:34 +00:00
Bradley Cicenas
b3aa291182 update termbox-go dependency 2018-03-09 05:15:55 +00:00
Bradley Cicenas
051b474bf0 dep updates 2018-02-23 13:57:59 +09:30
Bradley Cicenas
fac6632459 build image from go 1.10 2018-02-22 14:58:21 +09:30
Bradley Cicenas
1c7cf98e58 handle window resize in help dialog 2018-02-02 15:21:33 +00:00
Bradley Cicenas
44a54e070d combine conditionals 2018-02-01 16:55:23 +00:00
Bradley Cicenas
10b9a6c013 include windows build 2018-01-31 17:05:54 +00:00
Bradley Cicenas
a3b67e4607 add available connectors to help dialog 2018-01-30 07:54:13 -03:00
Bradley Cicenas
ac1ce18143 refactor enabled connectors 2018-01-29 12:47:10 +00:00
bradley
01a305d326 Update README.md
escape brackets
2018-01-11 15:58:37 -03:00
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
68 changed files with 1969 additions and 374 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

1
.gitignore vendored
View File

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

View File

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

View File

@@ -5,29 +5,32 @@ EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
clean:
rm -rf _build/ _release/
rm -rf _build/ release/
build:
glide install
go mod download
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-dev:
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
build-all:
mkdir -p build
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
mkdir -p _build
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
GOOS=windows GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64
cd _build; sha256sum * > sha256sums.txt
image:
docker build -t ctop -f Dockerfile .
release:
mkdir _release
mkdir 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) \
$(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:
<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.
@@ -20,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux
```bash
sudo wget https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-linux-amd64 -O /usr/local/bin/ctop
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop
```
@@ -31,7 +31,7 @@ brew install ctop
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-darwin-amd64
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-darwin-amd64
sudo chmod +x /usr/local/bin/ctop
```
@@ -54,32 +54,41 @@ 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.
### 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
Option | Description
--- | ---
-a | show active containers only
-f <string> | set an initial filter string
-f \<string\> | set an initial filter string
-h | display help dialog
-i | invert default colors
-r | reverse container sort order
-s | select initial container sort field
-scale-cpu | show cpu as % of system total
-v | output version information and exit
### Keybindings
Key | Action
--- | ---
\<enter\> | Open container menu
a | Toggle display of all (running and non-running) containers
f | Filter displayed containers (`esc` to clear when open)
H | Toggle ctop header
h | Open help dialog
s | Select container sort field
r | Reverse container sort order
o | Open single view
l | View container logs (`t` to toggle timestamp when open)
S | Save current configuration to file
q | Quit ctop
[build]: _docs/build.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"
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"

View File

@@ -1 +1 @@
0.6.1
0.7.1

View File

@@ -1,6 +1,6 @@
# 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
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
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
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

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,11 +45,14 @@ var ColorMap = map[string]ui.Attribute{
"par.text.hi": ui.ColorBlack,
"sparkline.line.fg": ui.ColorGreen,
"sparkline.title.fg": ui.ColorWhite,
"status.ok": ui.ColorGreen,
"status.warn": ui.ColorYellow,
"status.danger": ui.ColorRed,
}
func InvertColorMap() {
re := regexp.MustCompile(".*.fg")
for k, _ := range ColorMap {
for k := range ColorMap {
if re.FindAllString(k, 1) != nil {
ColorMap[k] = ui.ColorBlack
}

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 File struct {
Options map[string]string `toml:"options"`
Toggles map[string]bool `toml:"toggles"`
}
func exportConfig() File {
c := File{
Options: make(map[string]string),
Toggles: make(map[string]bool),
}
for _, p := range GlobalParams {
c.Options[p.Key] = p.Val
}
for _, sw := range GlobalSwitches {
c.Toggles[sw.Key] = sw.Val
}
return c
}
func Read() error {
var config File
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

@@ -12,6 +12,11 @@ var params = []*Param{
Val: "state",
Label: "Container Sort Field",
},
&Param{
Key: "namespace",
Val: "state",
Label: "Kubernetes namespace for monitoring",
},
}
type Param struct {

View File

@@ -5,17 +5,27 @@ var switches = []*Switch{
&Switch{
Key: "sortReversed",
Val: false,
Label: "Reverse Sort Order",
Label: "Reverse sort order",
},
&Switch{
Key: "allContainers",
Val: true,
Label: "Show All Containers",
Label: "Show all containers",
},
&Switch{
Key: "fullRowCursor",
Val: true,
Label: "Highlight entire cursor row (vs. name only)",
},
&Switch{
Key: "enableHeader",
Val: true,
Label: "Enable Status Header",
Label: "Enable status header",
},
&Switch{
Key: "scaleCpu",
Val: false,
Label: "Show CPU as %% of system total",
},
}
@@ -40,10 +50,18 @@ func GetSwitchVal(k string) bool {
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
func Toggle(k string) {
sw := GetSwitch(k)
newVal := sw.Val != true
newVal := !sw.Val
log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
sw.Val = newVal
//log.Errorf("ignoring toggle for non-existant switch: %s", k)

View File

@@ -1,6 +1,7 @@
package collector
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient"
)
@@ -15,13 +16,15 @@ type Docker struct {
done chan bool
lastCpu float64
lastSysCpu float64
scaleCpu bool
}
func NewDocker(client *api.Client, id string) *Docker {
return &Docker{
Metrics: models.Metrics{},
id: id,
client: client,
Metrics: models.Metrics{},
id: id,
client: client,
scaleCpu: config.GetSwitchVal("scaleCpu"),
}
}
@@ -66,7 +69,7 @@ func (c *Docker) Stream() chan models.Metrics {
}
func (c *Docker) Logs() LogCollector {
return &DockerLogs{c.id, c.client, make(chan bool)}
return NewDockerLogs(c.id, c.client)
}
// Stop collector
@@ -82,14 +85,18 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
if c.scaleCpu {
c.CPUUtil = round((cpudiff / syscpudiff * 100))
} else {
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
}
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
}
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.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}

View File

@@ -17,6 +17,14 @@ type DockerLogs struct {
done chan bool
}
func NewDockerLogs(id string, client *api.Client) *DockerLogs {
return &DockerLogs{
id: id,
client: client,
done: make(chan bool),
}
}
func (l *DockerLogs) Stream() chan models.Log {
r, w := io.Pipe()
logCh := make(chan models.Log)
@@ -40,7 +48,7 @@ func (l *DockerLogs) Stream() chan models.Log {
for scanner.Scan() {
parts := strings.Split(scanner.Text(), " ")
ts := l.parseTime(parts[0])
logCh <- models.Log{ts, strings.Join(parts[1:], " ")}
logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
}
}()
@@ -50,15 +58,15 @@ func (l *DockerLogs) Stream() chan models.Log {
if err != nil {
log.Errorf("error reading container logs: %s", err)
}
log.Infof("log reader stopped for container: %s", l.id)
}()
go func() {
select {
case <-l.done:
cancel()
}
<-l.done
cancel()
}()
log.Infof("log reader started for container: %s", l.id)
return logCh
}

View File

@@ -0,0 +1,128 @@
package collector
import (
"time"
"k8s.io/metrics/pkg/apis/metrics/v1alpha1"
clientset "k8s.io/metrics/pkg/client/clientset/versioned"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
"k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
// Kubernetes collector
type Kubernetes struct {
models.Metrics
name string
client clientset.Interface
clientset *kubernetes.Clientset
running bool
stream chan models.Metrics
done chan bool
lastCpu float64
lastSysCpu float64
scaleCpu bool
}
func NewKubernetes(client *kubernetes.Clientset, name string) *Kubernetes {
return &Kubernetes{
Metrics: models.Metrics{},
name: name,
client: clientset.New(client.RESTClient()),
clientset: client,
scaleCpu: config.GetSwitchVal("scaleCpu"),
}
}
func (k *Kubernetes) Start() {
k.done = make(chan bool)
k.stream = make(chan models.Metrics)
go func() {
k.running = false
for {
result := &v1alpha1.PodMetrics{}
err := k.clientset.RESTClient().Get().AbsPath("/api/v1/namespaces/kube-system/services/http:heapster:/proxy/apis/metrics/v1alpha1/namespaces/" + config.GetVal("namespace") + "/pods/" + k.name).Do().Into(result)
if err != nil {
log.Errorf("has error %s here %s", k.name, err.Error())
time.Sleep(1 * time.Second)
continue
}
k.ReadCPU(result)
k.ReadMem(result)
k.stream <- k.Metrics
}
}()
k.running = true
log.Infof("collector started for container: %s", k.name)
}
func (c *Kubernetes) Running() bool {
return c.running
}
func (c *Kubernetes) Stream() chan models.Metrics {
return c.stream
}
func (c *Kubernetes) Logs() LogCollector {
return NewKubernetesLogs(c.name, c.clientset)
}
// Stop collector
func (c *Kubernetes) Stop() {
c.done <- true
}
func (k *Kubernetes) ReadCPU(metrics *v1alpha1.PodMetrics) {
all := int64(0)
for _, c := range metrics.Containers {
v := c.Usage[v1.ResourceCPU]
all += v.Value()
}
if all != 0 {
k.CPUUtil = round(float64(all))
}
}
func (k *Kubernetes) ReadMem(metrics *v1alpha1.PodMetrics) {
all := int64(0)
for _, c := range metrics.Containers {
v := c.Usage[v1.ResourceMemory]
a, ok := v.AsInt64()
if ok {
all += a
}
}
k.MemUsage = all
k.MemLimit = int64(0)
//k.MemPercent = percent(float64(k.MemUsage), float64(k.MemLimit))
}
//func (c *Kubernetes) ReadNet(stats *api.Stats) {
// var rx, tx int64
// for _, network := range stats.Networks {
// rx += int64(network.RxBytes)
// tx += int64(network.TxBytes)
// }
// c.NetRx, c.NetTx = rx, tx
//}
//
//func (c *Kubernetes) ReadIO(stats *api.Stats) {
// var read, write int64
// for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
// if blk.Op == "Read" {
// read = int64(blk.Value)
// }
// if blk.Op == "Write" {
// write = int64(blk.Value)
// }
// }
// c.IOBytesRead, c.IOBytesWrite = read, write
//}

View File

@@ -0,0 +1,78 @@
package collector
import (
"time"
"github.com/bcicen/ctop/models"
"k8s.io/client-go/kubernetes"
)
type KubernetesLogs struct {
id string
client *kubernetes.Clientset
done chan bool
}
func NewKubernetesLogs(id string, client *kubernetes.Clientset) *KubernetesLogs {
return &KubernetesLogs{
id: id,
client: client,
done: make(chan bool),
}
}
func (l *KubernetesLogs) Stream() chan models.Log {
//r, w := io.Pipe()
logCh := make(chan models.Log)
//ctx, cancel := context.WithCancel(context.Background())
//opts := api.LogsOptions{
// Context: ctx,
// Container: l.id,
// OutputStream: w,
// ErrorStream: w,
// Stdout: true,
// Stderr: true,
// Tail: "10",
// Follow: true,
// Timestamps: true,
//}
//// read io pipe into channel
//go func() {
// scanner := bufio.NewScanner(r)
// for scanner.Scan() {
// parts := strings.Split(scanner.Text(), " ")
// ts := l.parseTime(parts[0])
// logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
// }
//}()
//// connect to container log stream
//go func() {
// err := l.client.Logs(opts)
// if err != nil {
// log.Errorf("error reading container logs: %s", err)
// }
// log.Infof("log reader stopped for container: %s", l.id)
//}()
//go func() {
// <-l.done
// cancel()
//}()
log.Infof("log reader started for container: %s", l.id)
return logCh
}
func (l *KubernetesLogs) Stop() { l.done <- true }
func (l *KubernetesLogs) parseTime(s string) time.Time {
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
}
return ts
}

View File

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

View File

@@ -1,4 +1,4 @@
// +build !darwin
// +build linux
package collector

View File

@@ -1,10 +1,11 @@
// +build !darwin
// +build linux
package collector
import (
"time"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
@@ -21,6 +22,7 @@ type Runc struct {
interval int // collection interval, in seconds
lastCpu float64
lastSysCpu float64
scaleCpu bool
}
func NewRunc(libc libcontainer.Container) *Runc {
@@ -29,6 +31,7 @@ func NewRunc(libc libcontainer.Container) *Runc {
id: libc.ID(),
libc: libc,
interval: 1,
scaleCpu: config.GetSwitchVal("scaleCpu"),
}
return c
}
@@ -90,7 +93,11 @@ func (c *Runc) ReadCPU(stats *cgroups.Stats) {
cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
if c.scaleCpu {
c.CPUUtil = round((cpudiff / syscpudiff * 100))
} else {
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
}
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)

View File

@@ -6,10 +6,13 @@ import (
"sync"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
)
func init() { enabled["docker"] = NewDocker }
type Docker struct {
client *api.Client
containers map[string]*container.Container
@@ -45,8 +48,11 @@ func (cm *Docker) watchEvents() {
if e.Type != "container" {
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)
cm.needsRefresh <- e.ID
case "destroy":
@@ -74,6 +80,17 @@ func portsFormat(ports map[api.Port][]api.PortBinding) string {
return strings.Join(append(exposed, published...), "\n")
}
func ipsFormat(networks map[string]api.ContainerNetwork) string {
var ips []string
for k, v := range networks {
s := fmt.Sprintf("%s:%s", k, v.IPAddress)
ips = append(ips, s)
}
return strings.Join(ips, "\n")
}
func (cm *Docker) refresh(c *container.Container) {
insp := cm.inspect(c.Id)
// remove container if no longer exists
@@ -83,15 +100,20 @@ func (cm *Docker) refresh(c *container.Container) {
}
c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image)
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", insp.State.Health.Status)
for _, env := range insp.Config.Env {
c.SetMeta("[ENV-VAR]", env)
}
c.SetState(insp.State.Status)
}
func (cm *Docker) inspect(id string) *api.Container {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, ok := err.(*api.NoSuchContainer); ok == false {
if _, ok := err.(*api.NoSuchContainer); !ok {
log.Errorf(err.Error())
}
}
@@ -128,8 +150,10 @@ func (cm *Docker) MustGet(id string) *container.Container {
if !ok {
// create collector
collector := collector.NewDocker(cm.client, id)
// create manager
manager := manager.NewDocker(cm.client, id)
// create container
c = container.New(id, collector)
c = container.New(id, collector, manager)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
@@ -159,6 +183,7 @@ func (cm *Docker) All() (containers container.Containers) {
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()

View File

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

View File

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

218
connector/kubernetes.go Normal file
View File

@@ -0,0 +1,218 @@
package connector
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
bcfg "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func init() { enabled["kubernetes"] = NewKubernetes }
type Kubernetes struct {
namespace string
clientset *kubernetes.Clientset
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewKubernetes() Connector {
var kubeconfig string
//if home := homeDir(); home != "" {
// kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
//} else {
// kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
//}
//flag.Parse()
kubeconfig = filepath.Join(homeDir(), ".kube", "config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Error(err.Error())
return nil
}
// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Error(err.Error())
return nil
}
// init docker client
k := &Kubernetes{
clientset: clientset,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
namespace: bcfg.GetVal("namespace"),
}
go k.Loop()
k.refreshAll()
go k.watchEvents()
return k
}
func (k *Kubernetes) watchEvents() {
for {
log.Info("kubernetes event listener starting")
allEvents, err := k.clientset.CoreV1().Events(k.namespace).List(metav1.ListOptions{})
if err != nil {
log.Error(err.Error())
return
}
for _, e := range allEvents.Items {
if e.Kind != "pod" {
continue
}
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.UID)
k.needsRefresh <- e.Name
case "destroy":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.UID)
k.delByID(e.Name)
default:
log.Debugf("handling docker event: %v", e)
k.needsRefresh <- e.Name
}
}
time.Sleep(1 * time.Second)
}
}
func (k *Kubernetes) Loop() {
for id := range k.needsRefresh {
c := k.MustGet(id)
k.refresh(c)
}
}
// Get a single container, creating one anew if not existing
func (k *Kubernetes) MustGet(name string) *container.Container {
c, ok := k.Get(name)
// append container struct for new containers
if !ok {
// create collector
collector := collector.NewKubernetes(k.clientset, name)
// create manager
manager := manager.NewKubernetes(k.clientset, name)
// create container
c = container.New(name, collector, manager)
k.lock.Lock()
k.containers[name] = c
k.lock.Unlock()
}
return c
}
func (k *Kubernetes) refresh(c *container.Container) {
insp := k.inspect(c.Id)
// remove container if no longer exists
if insp == nil {
k.delByID(c.Id)
return
}
c.SetMeta("name", insp.Name)
if len(insp.Spec.Containers) >= 1 {
c.SetMeta("image", insp.Spec.Containers[0].Image)
c.SetMeta("ports", k8sPort(insp.Spec.Containers[0].Ports))
for _, env := range insp.Spec.Containers[0].Env {
c.SetMeta("[ENV-VAR]", env.Name+"="+env.Value)
}
}
c.SetMeta("IPs", insp.Status.PodIP)
c.SetMeta("created", insp.CreationTimestamp.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", string(insp.Status.Phase))
c.SetState("running")
}
func k8sPort(ports []v1.ContainerPort) string {
str := []string{}
for _, p := range ports {
str = append(str, fmt.Sprintf("%s:%d -> %d", p.HostIP, p.HostPort, p.ContainerPort))
}
return strings.Join(str, "\n")
}
func (k *Kubernetes) inspect(id string) *v1.Pod {
p, err := k.clientset.CoreV1().Pods(k.namespace).Get(id, metav1.GetOptions{})
if err != nil {
if _, ok := err.(*api.NoSuchContainer); !ok {
log.Errorf(err.Error())
}
}
return p
}
// Remove containers by ID
func (k *Kubernetes) delByID(name string) {
k.lock.Lock()
delete(k.containers, name)
k.lock.Unlock()
log.Infof("removed dead container: %s", name)
}
func (k *Kubernetes) Get(name string) (c *container.Container, ok bool) {
k.lock.Lock()
c, ok = k.containers[name]
k.lock.Unlock()
return
}
// Mark all container IDs for refresh
func (k *Kubernetes) refreshAll() {
allPods, err := k.clientset.CoreV1().Pods(k.namespace).List(metav1.ListOptions{})
if err != nil {
log.Error(err.Error())
return
}
for _, pod := range allPods.Items {
c := k.MustGet(pod.Name)
c.SetMeta("uid", string(pod.UID))
c.SetMeta("name", pod.Name)
if pod.Initializers != nil && pod.Initializers.Result != nil {
c.SetState(pod.Initializers.Result.Status)
} else {
c.SetState(string(pod.Status.Phase))
}
k.needsRefresh <- c.Id
}
}
func (k *Kubernetes) All() (containers container.Containers) {
k.lock.Lock()
for _, c := range k.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
k.lock.Unlock()
return containers
}
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}
return os.Getenv("USERPROFILE") // windows
}

View File

@@ -2,22 +2,31 @@ package connector
import (
"fmt"
"sort"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
var (
log = logging.Init()
enabled = make(map[string]func() Connector)
)
// return names for all enabled connectors on the current platform
func Enabled() (a []string) {
for k, _ := range enabled {
a = append(a, k)
}
sort.Strings(a)
return a
}
func ByName(s string) (Connector, error) {
if _, ok := enabled[s]; !ok {
msg := fmt.Sprintf("invalid connector type \"%s\"\nconnector must be one of:", s)
for k, _ := range enabled {
msg += fmt.Sprintf("\n %s", k)
}
return nil, fmt.Errorf(msg)
if cfn, ok := enabled[s]; ok {
return cfn(), nil
}
return enabled[s](), nil
return nil, fmt.Errorf("invalid connector type \"%s\"", s)
}
type Connector interface {

View File

@@ -0,0 +1,65 @@
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
}
func (dc *Docker) Pause() error {
if err := dc.client.PauseContainer(dc.id); err != nil {
return fmt.Errorf("cannot pause container: %v", err)
}
return nil
}
func (dc *Docker) Unpause() error {
if err := dc.client.UnpauseContainer(dc.id); err != nil {
return fmt.Errorf("cannot unpause container: %v", err)
}
return nil
}
func (dc *Docker) Restart() error {
if err := dc.client.RestartContainer(dc.id, 3); err != nil {
return fmt.Errorf("cannot restart container: %v", err)
}
return nil
}

View File

@@ -0,0 +1,64 @@
package manager
import (
"k8s.io/client-go/kubernetes"
)
type Kubernetes struct {
id string
client *kubernetes.Clientset
}
func NewKubernetes(client *kubernetes.Clientset, id string) *Kubernetes {
return &Kubernetes{
id: id,
client: client,
}
}
func (dc *Kubernetes) 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 *Kubernetes) Stop() error {
//if err := dc.client.StopContainer(dc.id, 3); err != nil {
// return fmt.Errorf("cannot stop container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Remove() error {
//if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
// return fmt.Errorf("cannot remove container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Pause() error {
//if err := dc.client.PauseContainer(dc.id); err != nil {
// return fmt.Errorf("cannot pause container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Unpause() error {
//if err := dc.client.UnpauseContainer(dc.id); err != nil {
// return fmt.Errorf("cannot unpause container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Restart() error {
//if err := dc.client.RestartContainer(dc.id, 3); err != nil {
// return fmt.Errorf("cannot restart container: %v", err)
//}
return nil
}

10
connector/manager/main.go Normal file
View File

@@ -0,0 +1,10 @@
package manager
type Manager interface {
Start() error
Stop() error
Remove() error
Pause() error
Unpause() error
Restart() error
}

31
connector/manager/mock.go Normal file
View File

@@ -0,0 +1,31 @@
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
}
func (m *Mock) Pause() error {
return nil
}
func (m *Mock) Unpause() error {
return nil
}
func (m *Mock) Restart() error {
return nil
}

31
connector/manager/runc.go Normal file
View File

@@ -0,0 +1,31 @@
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
}
func (rc *Runc) Pause() error {
return nil
}
func (rc *Runc) Unpause() error {
return nil
}
func (rc *Runc) Restart() error {
return nil
}

View File

@@ -8,16 +8,19 @@ import (
"time"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid"
)
func init() { enabled["mock"] = NewMock }
type Mock struct {
containers container.Containers
}
func NewMock() *Mock {
func NewMock() Connector {
cs := &Mock{}
go cs.Init()
go cs.Loop()
@@ -40,7 +43,8 @@ func (cs *Mock) Init() {
func (cs *Mock) makeContainer(aggression int64) {
collector := collector.NewMock(aggression)
c := container.New(makeID(), collector)
manager := manager.NewMock()
c := container.New(makeID(), collector, manager)
c.SetMeta("name", makeName())
c.SetState(makeState())
cs.containers = append(cs.containers, c)

View File

@@ -1,4 +1,4 @@
// +build !darwin
// +build linux
package connector
@@ -11,11 +11,14 @@ import (
"time"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
)
func init() { enabled["runc"] = NewRunc }
type RuncOpts struct {
root string // runc root path
systemdCgroups bool // use systemd cgroups
@@ -175,7 +178,8 @@ func (cm *Runc) MustGet(id string) *container.Container {
collector := collector.NewRunc(libc)
// create container
c = container.New(id, collector)
manager := manager.NewRunc()
c = container.New(id, collector, manager)
name := libc.ID()
// set initial metadata

View File

@@ -2,6 +2,7 @@ package container
import (
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging"
@@ -12,6 +13,10 @@ var (
log = logging.Init()
)
const (
running = "running"
)
// Metrics and metadata representing a container
type Container struct {
models.Metrics
@@ -21,9 +26,10 @@ type Container struct {
Display bool // display this container in compact view
updater cwidgets.WidgetUpdater
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)
return &Container{
Metrics: models.NewMetrics(),
@@ -32,6 +38,7 @@ func New(id string, collector collector.Collector) *Container {
Widgets: widgets,
updater: widgets,
collector: collector,
manager: manager,
}
}
@@ -57,12 +64,12 @@ func (c *Container) GetMeta(k string) string {
func (c *Container) SetState(s string) {
c.SetMeta("state", s)
// start collector, if needed
if s == "running" && !c.collector.Running() {
if s == running && !c.collector.Running() {
c.collector.Start()
c.Read(c.collector.Stream())
}
// stop collector, if needed
if s != "running" && c.collector.Running() {
if s != running && c.collector.Running() {
c.collector.Stop()
}
}
@@ -85,3 +92,64 @@ func (c *Container) Read(stream chan models.Metrics) {
}()
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)
}
}
func (c *Container) Pause() {
if c.Meta["state"] == running {
if err := c.manager.Pause(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState("paused")
}
}
func (c *Container) Unpause() {
if c.Meta["state"] == "paused" {
if err := c.manager.Unpause(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState(running)
}
}
func (c *Container) Restart() {
if c.Meta["state"] == running {
if err := c.manager.Restart(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
}
}

View File

@@ -57,11 +57,11 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
// Set an initial cursor position, if possible
func (gc *GridCursor) Reset() {
for _, c := range gc.cSource.All() {
c.Widgets.Name.UnHighlight()
c.Widgets.UnHighlight()
}
if gc.Len() > 0 {
gc.selectedID = gc.filtered[0].Id
gc.filtered[0].Widgets.Name.Highlight()
gc.filtered[0].Widgets.Highlight()
}
}
@@ -109,9 +109,9 @@ func (gc *GridCursor) Up() {
active := gc.filtered[idx]
next := gc.filtered[idx-1]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
@@ -128,9 +128,9 @@ func (gc *GridCursor) Down() {
active := gc.filtered[idx]
next := gc.filtered[idx+1]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
@@ -151,9 +151,9 @@ func (gc *GridCursor) PgUp() {
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)
@@ -174,9 +174,9 @@ func (gc *GridCursor) PgDown() {
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)

View File

@@ -23,12 +23,22 @@ func (w *GaugeCol) Reset() {
w.Percent = 0
}
func (w *GaugeCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg")
w.PercentColor = ui.ThemeAttr("par.text.hi")
}
func (w *GaugeCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg")
w.PercentColor = ui.ThemeAttr("par.text.bg")
}
func colorScale(n int) ui.Attribute {
if n > 70 {
return ui.ColorRed
return ui.ThemeAttr("status.danger")
}
if n > 30 {
return ui.ColorYellow
return ui.ThemeAttr("status.warn")
}
return ui.ColorGreen
return ui.ThemeAttr("status.ok")
}

View File

@@ -23,10 +23,7 @@ func NewCompactGrid() *CompactGrid {
func (cg *CompactGrid) Align() {
y := cg.Y
if cg.Offset >= len(cg.Rows) {
cg.Offset = 0
}
if cg.Offset < 0 {
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
cg.Offset = 0
}
@@ -60,7 +57,5 @@ func (cg *CompactGrid) Buffer() ui.Buffer {
}
func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
for _, r := range rows {
cg.Rows = append(cg.Rows, r)
}
cg.Rows = append(cg.Rows, rows...)
}

View File

@@ -1,6 +1,7 @@
package compact
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
@@ -13,10 +14,11 @@ type Compact struct {
Name *TextCol
Cid *TextCol
Cpu *GaugeCol
Memory *GaugeCol
Mem *GaugeCol
Net *TextCol
IO *TextCol
Pids *TextCol
Bg *RowBg
X, Y int
Width int
Height int
@@ -32,10 +34,11 @@ func NewCompact(id string) *Compact {
Name: NewTextCol("-"),
Cid: NewTextCol(id),
Cpu: NewGaugeCol(),
Memory: NewGaugeCol(),
Mem: NewGaugeCol(),
Net: NewTextCol("-"),
IO: NewTextCol("-"),
Pids: NewTextCol("-"),
Bg: NewRowBg(),
X: 1,
Height: 1,
}
@@ -56,6 +59,8 @@ func (row *Compact) SetMeta(k, v string) {
row.Name.Set(v)
case "state":
row.Status.Set(v)
case "health":
row.Status.SetHealth(v)
}
}
@@ -70,7 +75,7 @@ func (row *Compact) SetMetrics(m models.Metrics) {
// Set gauges, counters to default unread values
func (row *Compact) Reset() {
row.Cpu.Reset()
row.Memory.Reset()
row.Mem.Reset()
row.Net.Reset()
row.IO.Reset()
row.Pids.Reset()
@@ -88,6 +93,8 @@ func (row *Compact) SetY(y int) {
if y == row.Y {
return
}
row.Bg.Y = y
for _, col := range row.all() {
col.SetY(y)
}
@@ -99,6 +106,10 @@ func (row *Compact) SetWidth(width int) {
return
}
x := row.X
row.Bg.SetX(x + colWidths[0] + 1)
row.Bg.SetWidth(width)
autoWidth := calcWidth(width)
for n, col := range row.all() {
if colWidths[n] != 0 {
@@ -117,11 +128,12 @@ func (row *Compact) SetWidth(width int) {
func (row *Compact) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(row.Bg.Buffer())
buf.Merge(row.Status.Buffer())
buf.Merge(row.Name.Buffer())
buf.Merge(row.Cid.Buffer())
buf.Merge(row.Cpu.Buffer())
buf.Merge(row.Memory.Buffer())
buf.Merge(row.Mem.Buffer())
buf.Merge(row.Net.Buffer())
buf.Merge(row.IO.Buffer())
buf.Merge(row.Pids.Buffer())
@@ -134,9 +146,50 @@ func (row *Compact) all() []ui.GridBufferer {
row.Name,
row.Cid,
row.Cpu,
row.Memory,
row.Mem,
row.Net,
row.IO,
row.Pids,
}
}
func (row *Compact) Highlight() {
row.Name.Highlight()
if config.GetSwitchVal("fullRowCursor") {
row.Bg.Highlight()
row.Cid.Highlight()
row.Cpu.Highlight()
row.Mem.Highlight()
row.Net.Highlight()
row.IO.Highlight()
row.Pids.Highlight()
}
}
func (row *Compact) UnHighlight() {
row.Name.UnHighlight()
if config.GetSwitchVal("fullRowCursor") {
row.Bg.UnHighlight()
row.Cid.UnHighlight()
row.Cpu.UnHighlight()
row.Mem.UnHighlight()
row.Net.UnHighlight()
row.IO.UnHighlight()
row.Pids.UnHighlight()
}
}
type RowBg struct {
*ui.Par
}
func NewRowBg() *RowBg {
bg := ui.NewPar("")
bg.Height = 1
bg.Border = false
bg.Bg = ui.ThemeAttr("par.text.bg")
return &RowBg{bg}
}
func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") }
func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }

View File

@@ -19,7 +19,7 @@ func (row *Compact) SetIO(read int64, write int64) {
}
func (row *Compact) SetPids(val int) {
label := fmt.Sprintf("%s", strconv.Itoa(val))
label := strconv.Itoa(val)
row.Pids.Set(label)
}
@@ -37,12 +37,12 @@ func (row *Compact) SetCPU(val 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 {
percent = 5
row.Memory.BarColor = ui.ColorBlack
row.Mem.BarColor = ui.ColorBlack
} 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
import (
"fmt"
ui "github.com/gizak/termui"
)
const (
mark = string('\u25C9')
vBar = string('\u25AE')
statusWidth = 3
mark = string('\u25C9')
healthMark = string('\u207A')
vBar = string('\u25AE') + string('\u25AE')
)
// Status indicator
type Status struct {
*ui.Par
*ui.Block
status []ui.Cell
health []ui.Cell
}
func NewStatus() *Status {
p := ui.NewPar(mark)
p.Border = false
p.Height = 1
p.Width = statusWidth
return &Status{p}
s := &Status{Block: ui.NewBlock()}
s.Height = 1
s.Border = false
s.Set("")
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) {
@@ -32,13 +46,38 @@ func (s *Status) Set(val string) {
switch val {
case "running":
color = ui.ColorGreen
color = ui.ThemeAttr("status.ok")
case "exited":
color = ui.ColorRed
color = ui.ThemeAttr("status.danger")
case "paused":
text = fmt.Sprintf("%s%s", vBar, vBar)
text = vBar
}
s.Text = text
s.TextFgColor = color
var cells []ui.Cell
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", "Succeeded":
color = ui.ThemeAttr("status.ok")
case "unhealthy", "Failed", "Unknown":
color = ui.ThemeAttr("status.danger")
case "starting", "Pending", "Running":
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

@@ -17,11 +17,13 @@ func NewTextCol(s string) *TextCol {
}
func (w *TextCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg")
w.TextFgColor = ui.ThemeAttr("par.text.hi")
w.TextBgColor = ui.ThemeAttr("par.text.fg")
}
func (w *TextCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg")
w.TextFgColor = ui.ThemeAttr("par.text.fg")
w.TextBgColor = ui.ThemeAttr("par.text.bg")
}

View File

@@ -4,6 +4,7 @@ package compact
import (
"fmt"
ui "github.com/gizak/termui"
)
@@ -28,7 +29,7 @@ func calcWidth(width int) int {
for _, w := range colWidths {
width -= w
if w == 0 {
staticCols += 1
staticCols++
}
}
return (width - spacing) / staticCols

View File

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

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

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

View File

@@ -1,4 +1,4 @@
package expanded
package single
type IntHist struct {
Val int // most current data point

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package expanded
package single
import (
"github.com/bcicen/ctop/logging"
@@ -12,31 +12,33 @@ var (
colWidth = [2]int{65, 0} // left,right column width
)
type Expanded struct {
type Single struct {
Info *Info
Net *Net
Cpu *Cpu
Mem *Mem
IO *IO
Env *Env
X, Y int
Width int
}
func NewExpanded(id string) *Expanded {
func NewSingle(id string) *Single {
if len(id) > 12 {
id = id[:12]
}
return &Expanded{
return &Single{
Info: NewInfo(id),
Net: NewNet(),
Cpu: NewCpu(),
Mem: NewMem(),
IO: NewIO(),
Env: NewEnv(),
Width: ui.TermWidth(),
}
}
func (e *Expanded) Up() {
func (e *Single) Up() {
if e.Y < 0 {
e.Y++
e.Align()
@@ -44,7 +46,7 @@ func (e *Expanded) Up() {
}
}
func (e *Expanded) Down() {
func (e *Single) Down() {
if e.Y > (ui.TermHeight() - e.GetHeight()) {
e.Y--
e.Align()
@@ -52,10 +54,16 @@ func (e *Expanded) Down() {
}
}
func (e *Expanded) SetWidth(w int) { e.Width = w }
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
func (e *Single) SetWidth(w int) { e.Width = w }
func (e *Single) SetMeta(k, v string) {
if k == "[ENV-VAR]" {
e.Env.Set(k, v)
} else {
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.Net.Update(m.NetRx, m.NetTx)
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
@@ -63,16 +71,17 @@ func (e *Expanded) SetMetrics(m models.Metrics) {
}
// Return total column height
func (e *Expanded) GetHeight() (h int) {
func (e *Single) GetHeight() (h int) {
h += e.Info.Height
h += e.Net.Height
h += e.Cpu.Height
h += e.Mem.Height
h += e.IO.Height
h += e.Env.Height
return h
}
func (e *Expanded) Align() {
func (e *Single) Align() {
// reset offset if needed
if e.GetHeight() <= ui.TermHeight() {
e.Y = 0
@@ -91,10 +100,7 @@ func (e *Expanded) Align() {
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
}
func calcWidth(w int) {
}
func (e *Expanded) Buffer() ui.Buffer {
func (e *Single) Buffer() ui.Buffer {
buf := ui.NewBuffer()
if e.Width < (colWidth[0] + colWidth[1]) {
ui.Clear()
@@ -106,16 +112,18 @@ func (e *Expanded) Buffer() ui.Buffer {
buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
buf.Merge(e.Env.Buffer())
return buf
}
func (e *Expanded) all() []ui.GridBufferer {
func (e *Single) all() []ui.GridBufferer {
return []ui.GridBufferer{
e.Info,
e.Cpu,
e.Mem,
e.Net,
e.IO,
e.Env,
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package expanded
package single
import (
"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

61
go.mod Normal file
View File

@@ -0,0 +1,61 @@
module github.com/bcicen/ctop
require (
github.com/Azure/go-ansiterm v0.0.0-20160622173216-fa152c58bc15 // indirect
github.com/BurntSushi/toml v0.3.0
github.com/Microsoft/go-winio v0.3.8 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/Sirupsen/logrus v0.0.0-20150423025312-26709e271410 // indirect
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd
github.com/coreos/go-systemd v0.0.0-20151104194251-b4a58d95188d // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v0.0.0-20170502054910-90d35abf7b35 // indirect
github.com/docker/go-connections v0.0.0-20170301234100-a2afab980204 // indirect
github.com/docker/go-units v0.3.2 // indirect
github.com/fsouza/go-dockerclient v0.0.0-20170307141636-318513eb1ab2
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gizak/termui v2.3.0+incompatible
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect
github.com/hashicorp/go-cleanhttp v0.0.0-20170211013415-3573b8b52aa7 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
github.com/json-iterator/go v1.1.5 // indirect
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
github.com/opencontainers/runc v0.1.1
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/seccomp/libseccomp-golang v0.0.0-20150813023252-1b506fc7c24e // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.2.2 // indirect
github.com/syndtr/gocapability v0.0.0-20150716010906-2c00daeb6c3b // indirect
github.com/vishvananda/netlink v0.0.0-20150820014904-1e2e08e8a2dc // indirect
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 // indirect
golang.org/x/oauth2 v0.0.0-20181128211412-28207608b838 // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/appengine v1.3.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
k8s.io/api v0.0.0-20181130031204-d04500c8c3dd
k8s.io/apimachinery v0.0.0-20181201231028-18a5ff3097b4
k8s.io/client-go v9.0.0+incompatible
k8s.io/klog v0.1.0 // indirect
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
sigs.k8s.io/yaml v1.1.0 // indirect
)
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5

74
grid.go
View File

@@ -2,8 +2,7 @@ package main
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/expanded"
"github.com/bcicen/ctop/cwidgets/single"
ui "github.com/gizak/termui"
)
@@ -18,6 +17,7 @@ func RedrawRows(clr bool) {
header.SetFilter(config.GetVal("filterStr"))
y += header.Height()
}
cGrid.SetY(y)
for _, c := range cursor.filtered {
@@ -33,14 +33,20 @@ func RedrawRows(clr bool) {
}
cGrid.Align()
ui.Render(cGrid)
}
func ExpandView(c *container.Container) {
func SingleView() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
ex := expanded.NewExpanded(c.Id)
ex := single.NewSingle(c.Id)
c.SetUpdater(ex)
ex.Align()
@@ -59,6 +65,7 @@ func ExpandView(c *container.Container) {
ui.Loop()
c.SetUpdater(c.Widgets)
return nil
}
func RefreshDisplay() {
@@ -70,14 +77,14 @@ func RefreshDisplay() {
}
func Display() bool {
var menu func()
var expand bool
var menu MenuFn
cGrid.SetWidth(ui.TermWidth())
ui.DefaultEvtStream.Hook(logEvent)
// initial draw
header.Align()
status.Align()
cursor.RefreshContainers()
RedrawRows(true)
@@ -94,7 +101,23 @@ func Display() bool {
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
expand = true
menu = ContainerMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<left>", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<right>", func(ui.Event) {
menu = SingleView
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/o", func(ui.Event) {
menu = SingleView
ui.StopLoop()
})
ui.Handle("/sys/kbd/a", func(ui.Event) {
@@ -119,13 +142,26 @@ func Display() bool {
menu = SortMenu
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) {
if log.StatusQueued() {
ui.StopLoop()
}
RefreshDisplay()
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
header.Align()
status.Align()
cursor.ScrollPage()
cGrid.SetWidth(ui.TermWidth())
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
@@ -133,16 +169,24 @@ func Display() bool {
})
ui.Loop()
if menu != nil {
menu()
return false
}
if expand {
c := cursor.Selected()
if c != nil {
ExpandView(c)
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 {
for menu != nil {
menu = menu()
}
return false
}
return true
}

84
install.sh Executable file
View File

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

View File

@@ -1,6 +1,7 @@
package logging
import (
"fmt"
"os"
"time"
@@ -20,11 +21,36 @@ var (
)
)
type statusMsg struct {
Text string
IsError bool
}
type CTopLogger struct {
*logging.Logger
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 {
if Log == nil {
logging.SetFormatter(format) // setup default formatter
@@ -32,6 +58,7 @@ func Init() *CTopLogger {
Log = &CTopLogger{
logging.MustGetLogger("ctop"),
logging.NewMemoryBackend(size),
[]statusMsg{},
}
if debugMode() {

View File

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

41
main.go
View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"runtime"
"strings"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector"
@@ -25,23 +26,27 @@ var (
cursor *GridCursor
cGrid *compact.CompactGrid
header *widgets.CTopHeader
status *widgets.StatusLine
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
fs = flag.NewFlagSet("ctop", flag.ExitOnError)
versionFlag = fs.Bool("version", false, "output version information and exit")
helpFlag = fs.Bool("h", false, "display this help dialog")
filterFlag = fs.String("f", "", "filter containers")
activeOnlyFlag = fs.Bool("a", false, "show active containers only")
sortFieldFlag = fs.String("s", "", "select container sort field")
reverseSortFlag = fs.Bool("r", false, "reverse container sort order")
invertFlag = fs.Bool("i", false, "invert default colors")
scaleCpu = fs.Bool("scale-cpu", false, "show cpu as % of system total")
connectorFlag = fs.String("connector", "docker", "container connector to use")
namespaceFlag = fs.String("n", "default", "Kubernetes namespace for monitoring")
)
func main() {
defer panicExit()
// parse command line arguments
var versionFlag = flag.Bool("v", false, "output version information and exit")
var helpFlag = flag.Bool("h", false, "display this help dialog")
var filterFlag = flag.String("f", "", "filter containers")
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
var sortFieldFlag = flag.String("s", "", "select container sort field")
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
var invertFlag = flag.Bool("i", false, "invert default colors")
var connectorFlag = flag.String("connector", "docker", "container connector to use")
flag.Parse()
fs.Parse(os.Args[1:])
if *versionFlag {
fmt.Println(versionStr)
@@ -56,8 +61,9 @@ func main() {
// init logger
log = logging.Init()
// init global config
// init global config and read config file if exists
config.Init()
config.Read()
// override default config values with command line flags
if *filterFlag != "" {
@@ -77,14 +83,20 @@ func main() {
config.Toggle("sortReversed")
}
if *scaleCpu {
config.Toggle("scaleCpu")
}
// init ui
if *invertFlag {
InvertColorMap()
}
config.Update("namespace", *namespaceFlag)
ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil {
panic(err)
}
tm.SetInputMode(tm.InputAlt)
defer Shutdown()
// init grid, cursor, header
@@ -95,6 +107,7 @@ func main() {
cursor = &GridCursor{cSource: conn}
cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader()
status = widgets.NewStatusLine()
for {
exit := Display()
@@ -128,7 +141,7 @@ func panicExit() {
}
}
var helpMsg = `ctop - container metric viewer
var helpMsg = `ctop - interactive container viewer
usage: ctop [options]
@@ -137,5 +150,7 @@ options:
func printHelp() {
fmt.Println(helpMsg)
flag.PrintDefaults()
fs.PrintDefaults()
fmt.Printf("\navailable connectors: ")
fmt.Println(strings.Join(connector.Enabled(), ", "))
}

226
menus.go
View File

@@ -1,6 +1,9 @@
package main
import (
"fmt"
"time"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/widgets"
@@ -8,17 +11,25 @@ import (
ui "github.com/gizak/termui"
)
// MenuFn executes a menu window, returning the next menu or nil
type MenuFn func() MenuFn
var helpDialog = []menu.Item{
menu.Item{"[a] - toggle display of all containers", ""},
menu.Item{"[f] - filter displayed containers", ""},
menu.Item{"[h] - open this help dialog", ""},
menu.Item{"[H] - toggle ctop header", ""},
menu.Item{"[s] - select container sort field", ""},
menu.Item{"[r] - reverse container sort order", ""},
menu.Item{"[q] - exit ctop", ""},
{"<enter> - open container menu", ""},
{"", ""},
{"[a] - toggle display of all containers", ""},
{"[f] - filter displayed containers", ""},
{"[h] - open this help dialog", ""},
{"[H] - toggle ctop header", ""},
{"[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.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
@@ -26,14 +37,18 @@ func HelpMenu() {
m := menu.NewMenu()
m.BorderLabel = "Help"
m.AddItems(helpDialog...)
ui.Render(m)
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Clear()
ui.Render(m)
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
return nil
}
func FilterMenu() {
func FilterMenu() MenuFn {
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
@@ -63,9 +78,10 @@ func FilterMenu() {
ui.StopLoop()
})
ui.Loop()
return nil
}
func SortMenu() {
func SortMenu() MenuFn {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
@@ -93,4 +109,192 @@ func SortMenu() {
ui.Render(m)
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"})
items = append(items, menu.Item{Val: "pause", Label: "pause"})
items = append(items, menu.Item{Val: "restart", Label: "restart"})
}
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"})
}
if c.Meta["state"] == "paused" {
items = append(items, menu.Item{Val: "unpause", Label: "unpause"})
}
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)
case "pause":
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
case "unpause":
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
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 = fmt.Sprintf("Logs [%s]", c.GetMeta("name"))
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 {
return &CTopHeader{
Time: headerPar(2, timeStr()),
Count: headerPar(27, "-"),
Filter: headerPar(47, ""),
Count: headerPar(24, "-"),
Filter: headerPar(40, ""),
bg: headerBg(),
}
}

View File

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

View File

@@ -10,7 +10,8 @@ type Padding [2]int // x,y padding
type Menu struct {
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
TextBgColor ui.Attribute
Selectable bool
@@ -82,9 +83,19 @@ func (m *Menu) Buffer() ui.Buffer {
var cell ui.Cell
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 {
x := m.X + m.padding[0]
y := m.Y + m.padding[1]
for _, ch := range item.Text() {
// invert bg/fg colors on currently selected row
if m.Selectable && n == m.cursorPos {
@@ -118,14 +129,22 @@ func (m *Menu) Down() {
func (m *Menu) calcSize() {
m.Width = 7 // minimum width
items := m.items
var height int
for _, i := range m.items {
s := i.Text()
if len(s) > m.Width {
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.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))
}
}