Compare commits

..

1 Commits

Author SHA1 Message Date
Kevin Schoon
47b27a7786 vendor dependencies with glide 2017-03-13 08:05:40 +11:00
86 changed files with 1014 additions and 4926 deletions

View File

@@ -1,19 +0,0 @@
version: 2
jobs:
build:
working_directory: ~/build
docker:
- image: circleci/golang:latest
steps:
- checkout
- setup_remote_docker:
version: 19.03.13
- 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

4
.gitignore vendored
View File

@@ -1,4 +0,0 @@
ctop
.idea
/vendor/
*.log

View File

@@ -1,17 +1,7 @@
FROM quay.io/vektorcloud/go:1.15 FROM quay.io/vektorcloud/glibc:latest
RUN apk add --no-cache make RUN ctop_url=$(wget -q -O - https://api.github.com/repos/bcicen/ctop/releases/latest | grep 'browser_' | cut -d\" -f4 |grep 'linux-amd64') && \
wget -q $ctop_url -O /ctop && \
chmod +x /ctop
WORKDIR /app
COPY go.mod .
RUN go mod download
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"] ENTRYPOINT ["/ctop"]

View File

@@ -1,37 +0,0 @@
NAME=ctop
VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD)
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
clean:
rm -rf _build/ release/
build:
go mod download
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-all:
mkdir -p _build
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
GOOS=linux GOARCH=ppc64le CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-ppc64le
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64
cd _build; sha256sum * > sha256sums.txt
run-dev:
rm -f ctop.sock ctop
go build -ldflags $(LD_FLAGS) -o ctop
CTOP_DEBUG=1 ./ctop
image:
docker build -t ctop -f Dockerfile .
release:
mkdir release
cp _build/* release
cd release; sha256sum --quiet --check sha256sums.txt && \
gh release create v$(VERSION) -d -t v$(VERSION) *
.PHONY: build

113
README.md
View File

@@ -1,118 +1,69 @@
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p> <p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>
# #
![release][release] ![homebrew][homebrew] ![macports][macports]
Top-like interface for container metrics 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 a [single container view][single_view] for inspecting a specific container. as well as an [expanded view][expanded_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` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases.
## Install ## Install
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform: Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
#### Debian/Ubuntu #### Linux
Maintained by a [third party](https://packages.azlux.fr/)
```bash
echo "deb http://packages.azlux.fr/debian/ buster main" | sudo tee /etc/apt/sources.list.d/azlux.list
wget -qO - https://azlux.fr/repo.gpg.key | sudo apt-key add -
sudo apt update
sudo apt install docker-ctop
```
#### Arch
`ctop` is available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
#### Linux (Generic)
```bash ```bash
sudo wget https://github.com/bcicen/ctop/releases/download/0.7.6/ctop-0.7.6-linux-amd64 -O /usr/local/bin/ctop wget https://github.com/bcicen/ctop/releases/download/v0.4.1/ctop-0.4.1-linux-amd64 -O ctop
sudo mv ctop /usr/local/bin/
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
#### OS X #### OS X
```bash ```bash
brew install ctop curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.4.1/ctop-0.4.1-darwin-amd64
``` sudo mv ctop /usr/local/bin/
or
```bash
sudo port install ctop
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/0.7.6/ctop-0.7.6-darwin-amd64
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
#### Docker or run via Docker:
```bash ```bash
docker run --rm -ti \ docker run -ti -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
--name=ctop \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop:latest
``` ```
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop/)
## Building ## Building
Build steps can be found [here][build]. To build `ctop` from source ensure you have a recent version of [glide](http://glide.sh/) installed.
```bash
cd $GOPATH/src/github.com/bcicen/ctop
glide install
```
## Usage ## Usage
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options. `ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
```bash
### Config file export DOCKER_HOST=tcp://127.0.0.1:4243
ctop
While running, use `S` to save the current filters, sort field, and other options to a default config path (`~/.config/ctop/config` on XDG systems, else `~/.ctop`). ```
Config file values will be loaded and applied the next time `ctop` is started.
### Options
Option | Description
--- | ---
`-a` | show active containers only
`-f <string>` | set an initial filter string
`-h` | display help dialog
`-i` | invert default colors
`-r` | reverse container sort order
`-s` | select initial container sort field
`-v` | output version information and exit
### Keybindings ### Keybindings
| Key | Action | Key | Action
| :----------------------: | ---------------------------------------------------------- | --- | ---
| <kbd>&lt;ENTER&gt;</kbd> | Open container menu | a | Toggle display of all (running and non-running) containers
| <kbd>a</kbd> | Toggle display of all (running and non-running) containers | f | Filter displayed containers
| <kbd>f</kbd> | Filter displayed containers (`esc` to clear when open) | H | Toggle ctop header
| <kbd>H</kbd> | Toggle ctop header | h | Open help dialog
| <kbd>h</kbd> | Open help dialog | s | Select container sort field
| <kbd>s</kbd> | Select container sort field | r | Reverse container sort order
| <kbd>r</kbd> | Reverse container sort order | q | Quit ctop
| <kbd>o</kbd> | Open single view |
| <kbd>l</kbd> | View container logs (`t` to toggle timestamp when open) |
| <kbd>e</kbd> | Exec Shell |
| <kbd>c</kbd> | Configure columns |
| <kbd>S</kbd> | Save current configuration to file |
| <kbd>q</kbd> | Quit ctop |
[build]: _docs/build.md [expanded_view]: _docs/expanded.md
[connectors]: _docs/connectors.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"
[macports]: https://repology.org/badge/version-for-repo/macports/ctop.svg?header=macports "ctop"
## Alternatives
See [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker.

View File

@@ -1 +1 @@
0.7.6 0.4.1

View File

@@ -1,21 +0,0 @@
# Build
To build `ctop` from source, simply clone the repo and run:
```bash
make build
```
To build a minimal Docker image containing only `ctop`:
```bash
make image
```
Now you can run your local image:
```bash
docker run --rm -ti \
--name ctop \
-v /var/run/docker.sock:/var/run/docker.sock \
ctop:latest
```

View File

@@ -1,26 +0,0 @@
# Connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.
Default connector behavior can be changed by setting the relevant environment variables.
## Docker
Default connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
#### Options
Var | Description
--- | ---
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 of container state (default: `/run/runc`)
#### Options
Var | Description
--- | ---
RUNC_ROOT | path to runc root for container state (default: `/run/runc`)
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups

View File

@@ -1,71 +0,0 @@
# Debug Mode
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.
## Quick Start
If running `ctop` via Docker, debug logging can be most easily enabled as below:
```bash
docker run -ti --rm \
--name=ctop \
-e CTOP_DEBUG=1 \
-e CTOP_DEBUG_TCP=1 \
-p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/vektorlab/ctop:latest
```
Log messages can be followed by connecting to the default listen address:
```bash
curl -s localhost:9000
```
example output:
```
15:06:43.881 ▶ NOTI 002 logger initialized
15:06:43.881 ▶ INFO 003 loaded config param: "filterStr": ""
15:06:43.881 ▶ INFO 004 loaded config param: "sortField": "state"
15:06:43.881 ▶ INFO 005 loaded config switch: "sortReversed": false
15:06:43.881 ▶ INFO 006 loaded config switch: "allContainers": true
15:06:43.881 ▶ INFO 007 loaded config switch: "enableHeader": true
15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...
...
```
## Unix Socket
Debug mode is enabled via the `CTOP_DEBUG` environment variable:
```bash
CTOP_DEBUG=1 ./ctop
```
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
```bash
socat unix-connect:./ctop.sock stdio
```
## TCP Logging Socket
In lieu of using a local unix socket, TCP logging can be enabled via the `CTOP_DEBUG_TCP` environment variable:
```bash
CTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop
```
A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)
## Log to file
You can also log to a file by specifying `CTOP_DEBUG_FILE=/path/to/ctop.log` environment variable:
```sh
CTOP_DEBUG=1 CTOP_DEBUG_FILE=ctop.log ./ctop
```
This is useful for GoLand to see logs right in debug panel:
* Edit Run configuration
* Go to Logs tab
* Specify this log file in "Log file to be shown in console".
Then during debugging you'll see the log tab in debug panel:
![Debug in GoLand](img/goland_debug.png)

4
_docs/expanded.md Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

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

24
circle.yml Normal file
View File

@@ -0,0 +1,24 @@
machine:
services:
- docker
dependencies:
override:
- docker info
- |
if [[ "$CIRCLE_BRANCH" == "master" ]]; then
docker build -t quay.io/vektorlab/ctop:latest .
else
docker build -t quay.io/vektorlab/ctop:${CIRCLE_BRANCH} .
fi
test:
override:
- docker run -t --entrypoint /bin/sh quay.io/vektorlab/ctop:latest -v
deployment:
hub:
branch: master
commands:
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
- docker push quay.io/vektorlab/ctop:latest

View File

@@ -1,10 +1,6 @@
package main package main
import ( import ui "github.com/gizak/termui"
"regexp"
ui "github.com/gizak/termui"
)
/* /*
Valid colors: Valid colors:
@@ -42,20 +38,6 @@ var ColorMap = map[string]ui.Attribute{
"mbarchart.text.fg": ui.ColorWhite, "mbarchart.text.fg": ui.ColorWhite,
"par.text.fg": ui.ColorWhite, "par.text.fg": ui.ColorWhite,
"par.text.bg": ui.ColorDefault, "par.text.bg": ui.ColorDefault,
"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() {
re := regexp.MustCompile(".*.fg")
for k := range ColorMap {
if re.FindAllString(k, 1) != nil {
ColorMap[k] = ui.ColorBlack
}
}
ColorMap["par.text.hi"] = ui.ColorWhite
} }

View File

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

View File

@@ -1,144 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
"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 {
// update columns param from working config
Update("columns", ColumnsString())
lock.RLock()
defer lock.RUnlock()
c := File{
Options: make(map[string]string),
Toggles: make(map[string]bool),
}
for _, p := range GlobalParams {
c.Options[p.Key] = p.Val
}
for _, sw := range GlobalSwitches {
c.Toggles[sw.Key] = sw.Val
}
return c
}
//
func Read() error {
var config 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)
}
// set working column config, if provided
colStr := GetVal("columns")
if len(colStr) > 0 {
var colNames []string
for _, s := range strings.Split(colStr, ",") {
s = strings.TrimSpace(s)
if s != "" {
colNames = append(colNames, s)
}
}
SetColumns(colNames)
}
return nil
}
func Write() (path string, err error) {
path, err = getConfigPath()
if err != nil {
return path, err
}
cfgdir := filepath.Dir(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)
}
}
// remove prior to writing new file
if err := os.Remove(path); err != nil {
if !os.IsNotExist(err) {
return path, err
}
}
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return path, fmt.Errorf("failed to open config for writing: %s", err)
}
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
}

View File

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

View File

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

View File

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

View File

@@ -1,105 +0,0 @@
package collector
import (
"bufio"
"context"
"io"
"strings"
"time"
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient"
)
type DockerLogs struct {
id string
client *api.Client
done chan bool
}
func NewDockerLogs(id string, client *api.Client) *DockerLogs {
return &DockerLogs{
id: id,
client: client,
done: make(chan bool),
}
}
func (l *DockerLogs) Stream() chan models.Log {
r, w := io.Pipe()
logCh := make(chan models.Log)
ctx, cancel := context.WithCancel(context.Background())
opts := api.LogsOptions{
Context: ctx,
Container: l.id,
OutputStream: w,
//ErrorStream: w,
Stdout: true,
Stderr: true,
Tail: "20",
Follow: true,
Timestamps: true,
RawTerminal: true,
}
// read io pipe into channel
go func() {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), " ", 2)
if len(parts) == 0 {
continue
}
if len(parts) < 2 {
logCh <- models.Log{Timestamp: l.parseTime(""), Message: parts[0]}
} else {
logCh <- models.Log{Timestamp: l.parseTime(parts[0]), Message: 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 *DockerLogs) Stop() { l.done <- true }
func (l *DockerLogs) parseTime(s string) time.Time {
ts, err := time.Parse(time.RFC3339Nano, s)
if err == nil {
return ts
}
ts, err2 := time.Parse(time.RFC3339Nano, l.stripPfx(s))
if err2 == nil {
return ts
}
log.Errorf("failed to parse container log: %s", err)
log.Errorf("failed to parse container log2: %s", err2)
return time.Now()
}
// attempt to strip message header prefix from a given raw docker log string
func (l *DockerLogs) stripPfx(s string) string {
b := []byte(s)
if len(b) > 8 {
return string(b[8:])
}
return s
}

View File

@@ -1,35 +0,0 @@
package collector
import (
"math"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
)
var log = logging.Init()
type LogCollector interface {
Stream() chan models.Log
Stop()
}
type Collector interface {
Stream() chan models.Metrics
Logs() LogCollector
Running() bool
Start()
Stop()
}
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}
// return rounded percentage
func percent(val float64, total float64) int {
if total <= 0 {
return 0
}
return round((val / total) * 100)
}

View File

@@ -1,31 +0,0 @@
package collector
import (
"time"
"github.com/bcicen/ctop/models"
)
const mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea, regem falli contexo pro peregrinorum heremo absconditi araneae meminerim deliciosas actionibus facere modico dura sonuerunt psalmi contra rerum, tempus mala anima volebant dura quae o modis."
type MockLogs struct {
done chan bool
}
func (l *MockLogs) Stream() chan models.Log {
logCh := make(chan models.Log)
go func() {
for {
select {
case <-l.done:
break
default:
logCh <- models.Log{Timestamp: time.Now(), Message: mockLog}
time.Sleep(250 * time.Millisecond)
}
}
}()
return logCh
}
func (l *MockLogs) Stop() { l.done <- true }

View File

@@ -1,45 +0,0 @@
// +build linux
package collector
import (
linuxproc "github.com/c9s/goprocinfo/linux"
)
var sysMemTotal = getSysMemTotal()
const (
clockTicksPerSecond uint64 = 100
nanoSecondsPerSecond = 1e9
)
func getSysMemTotal() int64 {
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
return int64(stat.MemTotal * 1024)
}
// return cumulative system cpu usage in nanoseconds
func getSysCPUUsage() uint64 {
stat, err := linuxproc.ReadStat("/proc/stat")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
sum := stat.CPUStatAll.User +
stat.CPUStatAll.Nice +
stat.CPUStatAll.System +
stat.CPUStatAll.Idle +
stat.CPUStatAll.IOWait +
stat.CPUStatAll.IRQ +
stat.CPUStatAll.SoftIRQ +
stat.CPUStatAll.Steal +
stat.CPUStatAll.Guest +
stat.CPUStatAll.GuestNice
return (sum * nanoSecondsPerSecond) / clockTicksPerSecond
}

View File

@@ -1,132 +0,0 @@
// +build linux
package collector
import (
"time"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/types"
"github.com/bcicen/ctop/models"
)
// Runc collector
type Runc struct {
models.Metrics
id string
libc libcontainer.Container
stream chan models.Metrics
done bool
running bool
interval int // collection interval, in seconds
lastCpu float64
lastSysCpu float64
}
func NewRunc(libc libcontainer.Container) *Runc {
c := &Runc{
Metrics: models.Metrics{},
id: libc.ID(),
libc: libc,
interval: 1,
}
return c
}
func (c *Runc) Running() bool {
return c.running
}
func (c *Runc) Start() {
c.done = false
c.stream = make(chan models.Metrics)
go c.run()
}
func (c *Runc) Stop() {
c.running = false
c.done = true
}
func (c *Runc) Stream() chan models.Metrics {
return c.stream
}
func (c *Runc) Logs() LogCollector {
return nil
}
func (c *Runc) run() {
c.running = true
defer close(c.stream)
log.Debugf("collector started for container: %s", c.id)
for {
stats, err := c.libc.Stats()
if err != nil {
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
break
}
c.ReadCPU(stats.CgroupStats)
c.ReadMem(stats.CgroupStats)
c.ReadNet(stats.Interfaces)
c.stream <- c.Metrics
if c.done {
break
}
time.Sleep(1 * time.Second)
}
c.running = false
}
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
u := stats.CpuStats.CpuUsage
ncpus := uint8(len(u.PercpuUsage))
total := float64(u.TotalUsage)
system := float64(getSysCPUUsage())
cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu
c.NCpus = ncpus
c.CPUUtil = percent(cpudiff, syscpudiff)
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
}
func (c *Runc) ReadMem(stats *cgroups.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
c.MemLimit = sysMemTotal
}
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {
var rx, tx int64
for _, network := range interfaces {
rx += int64(network.RxBytes)
tx += int64(network.TxBytes)
}
c.NetRx, c.NetTx = rx, tx
}
func (c *Runc) ReadIO(stats *cgroups.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

@@ -1,316 +0,0 @@
package connector
import (
"fmt"
"strings"
"sync"
"time"
"github.com/op/go-logging"
"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 }
var actionToStatus = map[string]string{
"start": "running",
"die": "exited",
"stop": "exited",
"pause": "paused",
"unpause": "running",
}
type StatusUpdate struct {
Cid string
Field string // "status" or "health"
Status string
}
type Docker struct {
client *api.Client
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
statuses chan StatusUpdate
closed chan struct{}
lock sync.RWMutex
}
func NewDocker() (Connector, error) {
// init docker client
client, err := api.NewClientFromEnv()
if err != nil {
return nil, err
}
cm := &Docker{
client: client,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
statuses: make(chan StatusUpdate, 60),
closed: make(chan struct{}),
lock: sync.RWMutex{},
}
// query info as pre-flight healthcheck
info, err := client.Info()
if err != nil {
return nil, err
}
log.Debugf("docker-connector ID: %s", info.ID)
log.Debugf("docker-connector Driver: %s", info.Driver)
log.Debugf("docker-connector Images: %d", info.Images)
log.Debugf("docker-connector Name: %s", info.Name)
log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
go cm.Loop()
go cm.LoopStatuses()
cm.refreshAll()
go cm.watchEvents()
return cm, nil
}
// Docker implements Connector
func (cm *Docker) Wait() struct{} { return <-cm.closed }
// Docker events watcher
func (cm *Docker) watchEvents() {
log.Info("docker event listener starting")
events := make(chan *api.APIEvents)
opts := api.EventsOptions{Filters: map[string][]string{
"type": {"container"},
"event": {"create", "start", "health_status", "pause", "unpause", "stop", "die", "destroy"},
},
}
cm.client.AddEventListenerWithOptions(opts, events)
for e := range events {
actionName := e.Action
switch actionName {
// most frequent event is a health checks
case "health_status: healthy", "health_status: unhealthy":
sepIdx := strings.Index(actionName, ": ")
healthStatus := e.Action[sepIdx+2:]
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=health_status id=%s %s", e.ID, healthStatus)
}
cm.statuses <- StatusUpdate{e.ID, "health", healthStatus}
case "create":
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=create id=%s", e.ID)
}
cm.needsRefresh <- e.ID
case "destroy":
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=destroy id=%s", e.ID)
}
cm.delByID(e.ID)
default:
// check if this action changes status e.g. start -> running
status := actionToStatus[actionName]
if status != "" {
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=%s id=%s %s", actionName, e.ID, status)
}
cm.statuses <- StatusUpdate{e.ID, "status", status}
}
}
}
log.Info("docker event listener exited")
close(cm.closed)
}
func portsFormat(ports map[api.Port][]api.PortBinding) string {
var exposed []string
var published []string
for k, v := range ports {
if len(v) == 0 {
exposed = append(exposed, string(k))
continue
}
for _, binding := range v {
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
published = append(published, s)
}
}
return strings.Join(append(exposed, published...), "\n")
}
func webPort(ports map[api.Port][]api.PortBinding) string {
for _, v := range ports {
if len(v) == 0 {
continue
}
for _, binding := range v {
publishedIp := binding.HostIP
if publishedIp == "0.0.0.0" {
publishedIp = "localhost"
}
publishedWebPort := fmt.Sprintf("%s:%s", publishedIp, binding.HostPort)
return publishedWebPort
}
}
return ""
}
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, found, failed := cm.inspect(c.Id)
if failed {
return
}
// remove container if no longer exists
if !found {
cm.delByID(c.Id)
return
}
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))
webPort := webPort(insp.NetworkSettings.Ports)
if webPort != "" {
c.SetMeta("Web Port", webPort)
}
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("uptime", calcUptime(insp))
c.SetMeta("health", insp.State.Health.Status)
c.SetMeta("[ENV-VAR]", strings.Join(insp.Config.Env, ";"))
c.SetState(insp.State.Status)
}
func (cm *Docker) inspect(id string) (insp *api.Container, found bool, failed bool) {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, notFound := err.(*api.NoSuchContainer); notFound {
return c, false, false
}
// other error e.g. connection failed
log.Errorf("%s (%T)", err.Error(), err)
return c, false, true
}
return c, true, false
}
func calcUptime(insp *api.Container) string {
endTime := insp.State.FinishedAt
if endTime.IsZero() {
endTime = time.Now()
}
uptime := endTime.Sub(insp.State.StartedAt)
return uptime.Truncate(time.Second).String()
}
// Mark all container IDs for refresh
func (cm *Docker) refreshAll() {
opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
log.Errorf("%s (%T)", err.Error(), err)
return
}
for _, i := range allContainers {
c := cm.MustGet(i.ID)
c.SetMeta("name", shortName(i.Names[0]))
c.SetState(i.State)
cm.needsRefresh <- c.Id
}
}
func (cm *Docker) Loop() {
for {
select {
case id := <-cm.needsRefresh:
c := cm.MustGet(id)
cm.refresh(c)
case <-cm.closed:
return
}
}
}
func (cm *Docker) LoopStatuses() {
for {
select {
case statusUpdate := <-cm.statuses:
c, _ := cm.Get(statusUpdate.Cid)
if c != nil {
if statusUpdate.Field == "health" {
c.SetMeta("health", statusUpdate.Status)
} else {
c.SetState(statusUpdate.Status)
}
}
case <-cm.closed:
return
}
}
}
// MustGet gets a single container, creating one anew if not existing
func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
// append container struct for new containers
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, manager)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
}
return c
}
// Docker implements Connector
func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
cm.lock.Unlock()
return c, ok
}
// Remove containers by ID
func (cm *Docker) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Docker implements Connector
func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}
// use primary container name
func shortName(name string) string {
return strings.TrimPrefix(name, "/")
}

View File

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

View File

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

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

View File

@@ -1,35 +0,0 @@
package manager
type Mock struct{}
func NewMock() *Mock {
return &Mock{}
}
func (m *Mock) Start() error {
return ActionNotImplErr
}
func (m *Mock) Stop() error {
return ActionNotImplErr
}
func (m *Mock) Remove() error {
return ActionNotImplErr
}
func (m *Mock) Pause() error {
return ActionNotImplErr
}
func (m *Mock) Unpause() error {
return ActionNotImplErr
}
func (m *Mock) Restart() error {
return ActionNotImplErr
}
func (m *Mock) Exec(cmd []string) error {
return ActionNotImplErr
}

View File

@@ -1,35 +0,0 @@
package manager
type Runc struct{}
func NewRunc() *Runc {
return &Runc{}
}
func (rc *Runc) Start() error {
return ActionNotImplErr
}
func (rc *Runc) Stop() error {
return ActionNotImplErr
}
func (rc *Runc) Remove() error {
return ActionNotImplErr
}
func (rc *Runc) Pause() error {
return ActionNotImplErr
}
func (rc *Runc) Unpause() error {
return ActionNotImplErr
}
func (rc *Runc) Restart() error {
return ActionNotImplErr
}
func (rc *Runc) Exec(cmd []string) error {
return ActionNotImplErr
}

View File

@@ -1,257 +0,0 @@
// +build linux
package connector
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"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
}
func NewRuncOpts() (RuncOpts, error) {
var opts RuncOpts
// read runc root path
root := os.Getenv("RUNC_ROOT")
if root == "" {
root = "/run/runc"
}
abs, err := filepath.Abs(root)
if err != nil {
return opts, err
}
opts.root = abs
// ensure runc root path is readable
_, err = ioutil.ReadDir(opts.root)
if err != nil {
return opts, err
}
if os.Getenv("RUNC_SYSTEMD_CGROUP") == "1" {
opts.systemdCgroups = true
}
return opts, nil
}
type Runc struct {
opts RuncOpts
factory libcontainer.Factory
containers map[string]*container.Container
libContainers map[string]libcontainer.Container
closed chan struct{}
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewRunc() (Connector, error) {
opts, err := NewRuncOpts()
if err != nil {
return nil, err
}
factory, err := getFactory(opts)
if err != nil {
return nil, err
}
cm := &Runc{
opts: opts,
factory: factory,
containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container),
closed: make(chan struct{}),
lock: sync.RWMutex{},
}
go func() {
for {
select {
case <-cm.closed:
return
case <-time.After(5 * time.Second):
cm.refreshAll()
}
}
}()
go cm.Loop()
return cm, nil
}
func (cm *Runc) GetLibc(id string) libcontainer.Container {
// return previously loaded container
libc, ok := cm.libContainers[id]
if ok {
return libc
}
// load container
libc, err := cm.factory.Load(id)
if err != nil {
// remove container if no longer exists
if lerr, ok := err.(libcontainer.Error); ok && lerr.Code() == libcontainer.ContainerNotExists {
cm.delByID(id)
} else {
log.Warningf("failed to read container: %s\n", err)
}
return nil
}
return libc
}
// update a ctop container from libcontainer
func (cm *Runc) refresh(id string) {
libc := cm.GetLibc(id)
if libc == nil {
return
}
c := cm.MustGet(id)
// remove container if entered destroyed state on last refresh
// this gives adequate time for the collector to be shut down
if c.GetMeta("state") == "destroyed" {
cm.delByID(id)
return
}
status, err := libc.Status()
if err != nil {
log.Warningf("failed to read status for container: %s\n", err)
} else {
c.SetState(status.String())
}
state, err := libc.State()
if err != nil {
log.Warningf("failed to read state for container: %s\n", err)
} else {
c.SetMeta("created", state.BaseState.Created.Format("Mon Jan 2 15:04:05 2006"))
}
conf := libc.Config()
c.SetMeta("rootfs", conf.Rootfs)
}
// Read runc root, creating any new containers
func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root)
if err != nil {
log.Errorf("%s (%T)", err.Error(), err)
close(cm.closed)
return
}
for _, i := range list {
if i.IsDir() {
name := i.Name()
// attempt to load
libc := cm.GetLibc(name)
if libc == nil {
continue
}
_ = cm.MustGet(i.Name()) // ensure container exists
}
}
// queue all existing containers for refresh
for id, _ := range cm.containers {
cm.needsRefresh <- id
}
log.Debugf("queued %d containers for refresh", len(cm.containers))
}
func (cm *Runc) Loop() {
for id := range cm.needsRefresh {
cm.refresh(id)
}
}
// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
if !ok {
libc := cm.GetLibc(id)
// create collector
collector := collector.NewRunc(libc)
// create container
manager := manager.NewRunc()
c = container.New(id, collector, manager)
name := libc.ID()
// set initial metadata
if len(name) > 12 {
name = name[0:12]
}
c.SetMeta("name", name)
// add to map
cm.lock.Lock()
cm.containers[id] = c
cm.libContainers[id] = libc
cm.lock.Unlock()
log.Debugf("saw new container: %s", id)
}
return c
}
// Remove containers by ID
func (cm *Runc) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
delete(cm.libContainers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Runc implements Connector
func (cm *Runc) Wait() struct{} { return <-cm.closed }
// Runc implements Connector
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Runc implements Connector
func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
cgroupManager := libcontainer.Cgroupfs
if opts.systemdCgroups {
if systemd.IsRunningSystemd() {
cgroupManager = libcontainer.SystemdCgroups
} else {
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
}
}
return libcontainer.New(opts.root, cgroupManager)
}

76
container.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/metrics"
)
// Metrics and metadata representing a container
type Container struct {
metrics.Metrics
Id string
Meta map[string]string
Widgets *compact.Compact
updater cwidgets.WidgetUpdater
collector metrics.Collector
display bool // display this container in compact view
}
func NewContainer(id string, collector metrics.Collector) *Container {
widgets := compact.NewCompact(id)
return &Container{
Metrics: metrics.NewMetrics(),
Id: id,
Meta: make(map[string]string),
Widgets: widgets,
updater: widgets,
collector: collector,
}
}
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
c.updater = u
for k, v := range c.Meta {
c.updater.SetMeta(k, v)
}
}
func (c *Container) SetMeta(k, v string) {
c.Meta[k] = v
c.updater.SetMeta(k, v)
}
func (c *Container) GetMeta(k string) string {
if v, ok := c.Meta[k]; ok {
return v
}
return ""
}
func (c *Container) SetState(s string) {
c.SetMeta("state", s)
// start collector, if needed
if s == "running" && !c.collector.Running() {
c.collector.Start()
c.Read(c.collector.Stream())
}
// stop collector, if needed
if s != "running" && c.collector.Running() {
c.collector.Stop()
}
}
// Read metric stream, updating widgets
func (c *Container) Read(stream chan metrics.Metrics) {
go func() {
for metrics := range stream {
c.Metrics = metrics
c.updater.SetMetrics(metrics)
}
log.Infof("reader stopped for container: %s", c.Id)
c.Metrics = metrics.NewMetrics()
c.Widgets.Reset()
}()
log.Infof("reader started for container: %s", c.Id)
}

View File

@@ -1,160 +0,0 @@
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"
"github.com/bcicen/ctop/models"
)
var (
log = logging.Init()
)
const (
running = "running"
)
// Metrics and metadata representing a container
type Container struct {
models.Metrics
Id string
Meta models.Meta
Widgets *compact.CompactRow
Display bool // display this container in compact view
updater cwidgets.WidgetUpdater
collector collector.Collector
manager manager.Manager
}
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
widgets := compact.NewCompactRow()
return &Container{
Metrics: models.NewMetrics(),
Id: id,
Meta: models.NewMeta("id", id[:12]),
Widgets: widgets,
updater: widgets,
collector: collector,
manager: manager,
}
}
func (c *Container) RecreateWidgets() {
c.SetUpdater(cwidgets.NullWidgetUpdater{})
c.Widgets = compact.NewCompactRow()
c.SetUpdater(c.Widgets)
}
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
c.updater = u
c.updater.SetMeta(c.Meta)
}
func (c *Container) SetMeta(k, v string) {
c.Meta[k] = v
c.updater.SetMeta(c.Meta)
}
func (c *Container) GetMeta(k string) string {
return c.Meta.Get(k)
}
func (c *Container) SetState(s string) {
c.SetMeta("state", s)
// start collector, if needed
if s == running && !c.collector.Running() {
c.collector.Start()
c.Read(c.collector.Stream())
}
// stop collector, if needed
if s != running && c.collector.Running() {
c.collector.Stop()
}
}
// Logs returns container log collector
func (c *Container) Logs() collector.LogCollector {
return c.collector.Logs()
}
// Read metric stream, updating widgets
func (c *Container) Read(stream chan models.Metrics) {
go func() {
for metrics := range stream {
c.Metrics = metrics
c.updater.SetMetrics(metrics)
}
log.Infof("reader stopped for container: %s", c.Id)
c.Metrics = models.NewMetrics()
c.Widgets.Reset()
}()
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
}
}
}
func (c *Container) Exec(cmd []string) error {
return c.manager.Exec(cmd)
}

129
cursor.go
View File

@@ -1,23 +1,24 @@
package main package main
import ( import (
"math"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
type GridCursor struct { type GridCursor struct {
selectedID string // id of currently selected container selectedID string // id of currently selected container
filtered container.Containers filtered Containers
cSuper *connector.ConnectorSuper cSource ContainerSource
isScrolling bool // toggled when actively scrolling }
func NewGridCursor() *GridCursor {
return &GridCursor{
cSource: NewDockerContainerSource(),
}
} }
func (gc *GridCursor) Len() int { return len(gc.filtered) } func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) Selected() *container.Container { func (gc *GridCursor) Selected() *Container {
idx := gc.Idx() idx := gc.Idx()
if idx < gc.Len() { if idx < gc.Len() {
return gc.filtered[idx] return gc.filtered[idx]
@@ -25,21 +26,15 @@ func (gc *GridCursor) Selected() *container.Container {
return nil return nil
} }
// Refresh containers from source, returning whether the quantity of // Refresh containers from source
// containers has changed and any error func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
func (gc *GridCursor) RefreshContainers() (bool, error) {
oldLen := gc.Len() oldLen := gc.Len()
gc.filtered = container.Containers{}
cSource, err := gc.cSuper.Get() // Containers filtered by display bool
if err != nil { gc.filtered = Containers{}
return true, err
}
// filter Containers by display bool
var cursorVisible bool var cursorVisible bool
for _, c := range cSource.All() { for _, c := range gc.cSource.All() {
if c.Display { if c.display {
if c.Id == gc.selectedID { if c.Id == gc.selectedID {
cursorVisible = true cursorVisible = true
} }
@@ -47,30 +42,31 @@ func (gc *GridCursor) RefreshContainers() (bool, error) {
} }
} }
if !cursorVisible || gc.selectedID == "" { if oldLen != gc.Len() {
gc.Reset() lenChanged = true
} }
return oldLen != gc.Len(), nil if !cursorVisible {
gc.Reset()
}
if gc.selectedID == "" {
gc.Reset()
}
return lenChanged
} }
// Set an initial cursor position, if possible // Set an initial cursor position, if possible
func (gc *GridCursor) Reset() { func (gc *GridCursor) Reset() {
cSource, err := gc.cSuper.Get() for _, c := range gc.cSource.All() {
if err != nil { c.Widgets.Name.UnHighlight()
return
}
for _, c := range cSource.All() {
c.Widgets.UnHighlight()
} }
if gc.Len() > 0 { if gc.Len() > 0 {
gc.selectedID = gc.filtered[0].Id gc.selectedID = gc.filtered[0].Id
gc.filtered[0].Widgets.Highlight() gc.filtered[0].Widgets.Name.Highlight()
} }
} }
// Idx returns current cursor index // Return current cursor index
func (gc *GridCursor) Idx() int { func (gc *GridCursor) Idx() int {
for n, c := range gc.filtered { for n, c := range gc.filtered {
if c.Id == gc.selectedID { if c.Id == gc.selectedID {
@@ -104,9 +100,6 @@ func (gc *GridCursor) ScrollPage() {
} }
func (gc *GridCursor) Up() { func (gc *GridCursor) Up() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx() idx := gc.Idx()
if idx <= 0 { // already at top if idx <= 0 { // already at top
return return
@@ -114,18 +107,15 @@ func (gc *GridCursor) Up() {
active := gc.filtered[idx] active := gc.filtered[idx]
next := gc.filtered[idx-1] next := gc.filtered[idx-1]
active.Widgets.UnHighlight() active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id gc.selectedID = next.Id
next.Widgets.Highlight() next.Widgets.Name.Highlight()
gc.ScrollPage() gc.ScrollPage()
ui.Render(cGrid) ui.Render(cGrid)
} }
func (gc *GridCursor) Down() { func (gc *GridCursor) Down() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx() idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom if idx >= gc.Len()-1 { // already at bottom
return return
@@ -133,65 +123,10 @@ func (gc *GridCursor) Down() {
active := gc.filtered[idx] active := gc.filtered[idx]
next := gc.filtered[idx+1] next := gc.filtered[idx+1]
active.Widgets.UnHighlight() active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id gc.selectedID = next.Id
next.Widgets.Highlight() next.Widgets.Name.Highlight()
gc.ScrollPage() gc.ScrollPage()
ui.Render(cGrid) ui.Render(cGrid)
} }
func (gc *GridCursor) PgUp() {
idx := gc.Idx()
if idx <= 0 { // already at top
return
}
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
if gc.pgCount() > 0 {
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
float64(0)))
}
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
func (gc *GridCursor) PgDown() {
idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom
return
}
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
if gc.pgCount() > 0 {
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
float64(gc.Len()-cGrid.MaxRows())))
}
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
// number of pages at current row count and term height
func (gc *GridCursor) pgCount() int {
pages := gc.Len() / cGrid.MaxRows()
if gc.Len()%cGrid.MaxRows() > 0 {
pages++
}
return pages
}

View File

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

View File

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

View File

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

View File

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

132
cwidgets/compact/main.go Normal file
View File

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

View File

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

View File

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

View File

@@ -1,94 +1,44 @@
package compact package compact
import ( import (
"github.com/bcicen/ctop/models" "fmt"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
const (
mark = string('\u25C9')
vBar = string('\u25AE')
statusWidth = 3
)
// Status indicator // Status indicator
type Status struct { type Status struct {
*ui.Block *ui.Par
status []ui.Cell
health []ui.Cell
} }
func NewStatus() CompactCol { func NewStatus() *Status {
s := &Status{ p := ui.NewPar(mark)
Block: ui.NewBlock(), p.Border = false
status: []ui.Cell{{Ch: ' '}}, p.Height = 1
health: []ui.Cell{{Ch: ' '}}, p.Width = statusWidth
} return &Status{p}
s.Height = 1
s.Border = false
return s
} }
func (s *Status) Buffer() ui.Buffer { func (s *Status) Set(val string) {
buf := s.Block.Buffer() // defaults
buf.Set(s.InnerX(), s.InnerY(), s.health[0]) text := mark
buf.Set(s.InnerX()+2, s.InnerY(), s.status[0])
return buf
}
func (s *Status) SetMeta(m models.Meta) {
s.setState(m.Get("state"))
s.setHealth(m.Get("health"))
}
// Status implements CompactCol
func (s *Status) Reset() {}
func (s *Status) SetMetrics(models.Metrics) {}
func (s *Status) Highlight() {}
func (s *Status) UnHighlight() {}
func (s *Status) Header() string { return "" }
func (s *Status) FixedWidth() int { return 3 }
func (s *Status) setState(val string) {
color := ui.ColorDefault color := ui.ColorDefault
var mark string
switch val { switch val {
case "":
return
case "created":
mark = "◉"
case "running": case "running":
mark = "⏵" color = ui.ColorGreen
color = ui.ThemeAttr("status.ok")
case "exited": case "exited":
mark = "⏹" color = ui.ColorRed
color = ui.ThemeAttr("status.danger")
case "paused": case "paused":
mark = "⏸" text = fmt.Sprintf("%s%s", vBar, vBar)
default:
mark = " "
log.Warningf("unknown status string: \"%v\"", val)
} }
s.status = ui.TextCells(mark, color, ui.ColorDefault) s.Text = text
} s.TextFgColor = color
func (s *Status) setHealth(val string) {
color := ui.ColorDefault
var mark string
switch val {
case "":
return
case "healthy":
mark = "☼"
color = ui.ThemeAttr("status.ok")
case "unhealthy":
mark = "⚠"
color = ui.ThemeAttr("status.danger")
case "starting":
mark = "◌"
color = ui.ThemeAttr("status.warn")
default:
mark = " "
log.Warningf("unknown health state string: \"%v\"", val)
}
s.health = ui.TextCells(mark, color, ui.ColorDefault)
} }

View File

@@ -1,147 +1,35 @@
package compact package compact
import ( import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
// Column that shows container's meta property i.e. name, id, image tc.
type MetaCol struct {
*TextCol
metaName string
}
func (w *MetaCol) SetMeta(m models.Meta) {
w.setText(m.Get(w.metaName))
}
func NewNameCol() CompactCol {
c := &MetaCol{NewTextCol("NAME"), "name"}
c.fWidth = 30
return c
}
func NewCIDCol() CompactCol {
c := &MetaCol{NewTextCol("CID"), "id"}
c.fWidth = 12
return c
}
func NewImageCol() CompactCol {
return &MetaCol{NewTextCol("IMAGE"), "image"}
}
func NewPortsCol() CompactCol {
return &MetaCol{NewTextCol("PORTS"), "ports"}
}
func NewIpsCol() CompactCol {
return &MetaCol{NewTextCol("IPs"), "IPs"}
}
func NewCreatedCol() CompactCol {
c := &MetaCol{NewTextCol("CREATED"), "created"}
c.fWidth = 19 // Year will be stripped e.g. "Thu Nov 26 07:44:03" without 2020 at end
return c
}
type NetCol struct {
*TextCol
}
func NewNetCol() CompactCol {
return &NetCol{NewTextCol("NET RX/TX")}
}
func (w *NetCol) SetMetrics(m models.Metrics) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.NetRx), cwidgets.ByteFormat64Short(m.NetTx))
w.setText(label)
}
type IOCol struct {
*TextCol
}
func NewIOCol() CompactCol {
return &IOCol{NewTextCol("IO R/W")}
}
func (w *IOCol) SetMetrics(m models.Metrics) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.IOBytesRead), cwidgets.ByteFormat64Short(m.IOBytesWrite))
w.setText(label)
}
type PIDCol struct {
*TextCol
}
func NewPIDCol() CompactCol {
w := &PIDCol{NewTextCol("PIDS")}
w.fWidth = 4
return w
}
func (w *PIDCol) SetMetrics(m models.Metrics) {
w.setText(fmt.Sprintf("%d", m.Pids))
}
type UptimeCol struct {
*TextCol
}
func NewUptimeCol() CompactCol {
return &UptimeCol{NewTextCol("UPTIME")}
}
func (w *UptimeCol) SetMeta(m models.Meta) {
w.Text = m.Get("uptime")
}
type TextCol struct { type TextCol struct {
*ui.Par *ui.Par
header string
fWidth int
} }
func NewTextCol(header string) *TextCol { func NewTextCol(s string) *TextCol {
p := ui.NewPar("-") p := ui.NewPar(s)
p.Border = false p.Border = false
p.Height = 1 p.Height = 1
p.Width = 20 p.Width = 20
return &TextCol{p}
return &TextCol{
Par: p,
header: header,
fWidth: 0,
}
} }
func (w *TextCol) Highlight() { func (w *TextCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg") w.TextFgColor = ui.ColorBlack
w.TextFgColor = ui.ThemeAttr("par.text.hi")
w.TextBgColor = ui.ThemeAttr("par.text.fg") w.TextBgColor = ui.ThemeAttr("par.text.fg")
} }
func (w *TextCol) UnHighlight() { func (w *TextCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg")
w.TextFgColor = ui.ThemeAttr("par.text.fg") w.TextFgColor = ui.ThemeAttr("par.text.fg")
w.TextBgColor = ui.ThemeAttr("par.text.bg") w.TextBgColor = ui.ThemeAttr("par.text.bg")
} }
// TextCol implements CompactCol func (w *TextCol) Reset() {
func (w *TextCol) Reset() { w.setText("-") } w.Text = "-"
func (w *TextCol) SetMeta(models.Meta) {} }
func (w *TextCol) SetMetrics(models.Metrics) {}
func (w *TextCol) Header() string { return w.header }
func (w *TextCol) FixedWidth() int { return w.fWidth }
func (w *TextCol) setText(s string) { func (w *TextCol) Set(s string) {
if w.fWidth > 0 && len(s) > w.fWidth {
s = s[0:w.fWidth]
}
w.Text = s w.Text = s
} }

View File

@@ -4,12 +4,17 @@ package compact
import ( import (
"fmt" "fmt"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
const colSpacing = 1 const colSpacing = 1
// Calculate per-column width, given total width and number of items
func calcWidth(width, items int) int {
spacing := colSpacing * items
return (width - statusWidth - spacing) / items
}
func centerParText(p *ui.Par) { func centerParText(p *ui.Par) {
var text string var text string
var padding string var padding string

View File

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

View File

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

35
cwidgets/expanded/info.go Normal file
View File

@@ -0,0 +1,35 @@
package expanded
import (
ui "github.com/gizak/termui"
)
var displayInfo = []string{"id", "name", "image", "state"}
type Info struct {
*ui.Table
data map[string]string
}
func NewInfo(id string) *Info {
p := ui.NewTable()
p.Height = 4
p.Width = colWidth[0]
p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false
i := &Info{p, make(map[string]string)}
i.Set("id", id)
return i
}
func (w *Info) Set(k, v string) {
w.data[k] = v
// rebuild rows
w.Rows = [][]string{}
for _, k := range displayInfo {
if v, ok := w.data[k]; ok {
w.Rows = append(w.Rows, []string{k, v})
}
}
w.Height = len(w.Rows) + 2
}

View File

@@ -1,8 +1,8 @@
package single package expanded
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models" "github.com/bcicen/ctop/metrics"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
@@ -12,86 +12,47 @@ var (
colWidth = [2]int{65, 0} // left,right column width colWidth = [2]int{65, 0} // left,right column width
) )
type Single struct { type Expanded struct {
Info *Info Info *Info
Net *Net Net *Net
Cpu *Cpu Cpu *Cpu
Mem *Mem Mem *Mem
IO *IO
Env *Env
X, Y int
Width int Width int
} }
func NewSingle() *Single { func NewExpanded(id string) *Expanded {
return &Single{ if len(id) > 12 {
Info: NewInfo(), id = id[:12]
}
return &Expanded{
Info: NewInfo(id),
Net: NewNet(), Net: NewNet(),
Cpu: NewCpu(), Cpu: NewCpu(),
Mem: NewMem(), Mem: NewMem(),
IO: NewIO(),
Env: NewEnv(),
Width: ui.TermWidth(), Width: ui.TermWidth(),
} }
} }
func (e *Single) Up() { func (e *Expanded) SetWidth(w int) {
if e.Y < 0 { e.Width = w
e.Y++
e.Align()
ui.Render(e)
}
} }
func (e *Single) Down() { func (e *Expanded) SetMeta(k, v string) {
if e.Y > (ui.TermHeight() - e.GetHeight()) {
e.Y--
e.Align()
ui.Render(e)
}
}
func (e *Single) SetWidth(w int) { e.Width = w }
func (e *Single) SetMeta(m models.Meta) {
for k, v := range m {
if k == "[ENV-VAR]" {
e.Env.Set(v)
} else {
e.Info.Set(k, v) e.Info.Set(k, v)
}
}
} }
func (e *Single) SetMetrics(m models.Metrics) { func (e *Expanded) SetMetrics(m metrics.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))
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
} }
// GetHeight returns total column height func (e *Expanded) Align() {
func (e *Single) GetHeight() (h int) { y := 0
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 *Single) Align() {
// reset offset if needed
if e.GetHeight() <= ui.TermHeight() {
e.Y = 0
}
y := e.Y
for _, i := range e.all() { for _, i := range e.all() {
i.SetY(y) i.SetY(y)
y += i.GetHeight() y += i.GetHeight()
} }
if e.Width > colWidth[0] { if e.Width > colWidth[0] {
colWidth[1] = e.Width - (colWidth[0] + 1) colWidth[1] = e.Width - (colWidth[0] + 1)
} }
@@ -99,7 +60,10 @@ func (e *Single) 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 (e *Single) Buffer() ui.Buffer { func calcWidth(w int) {
}
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()
@@ -110,19 +74,15 @@ func (e *Single) Buffer() ui.Buffer {
buf.Merge(e.Cpu.Buffer()) buf.Merge(e.Cpu.Buffer())
buf.Merge(e.Mem.Buffer()) buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer()) buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
buf.Merge(e.Env.Buffer())
return buf return buf
} }
func (e *Single) all() []ui.GridBufferer { func (e *Expanded) all() []ui.GridBufferer {
return []ui.GridBufferer{ return []ui.GridBufferer{
e.Info, e.Info,
e.Cpu, e.Cpu,
e.Mem, e.Mem,
e.Net, e.Net,
e.IO,
e.Env,
} }
} }

View File

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

View File

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

View File

@@ -2,20 +2,12 @@ package cwidgets
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models" "github.com/bcicen/ctop/metrics"
) )
var log = logging.Init() var log = logging.Init()
type WidgetUpdater interface { type WidgetUpdater interface {
SetMeta(models.Meta) SetMeta(string, string)
SetMetrics(models.Metrics) SetMetrics(metrics.Metrics)
} }
type NullWidgetUpdater struct{}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMeta(models.Meta) {}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}

View File

@@ -1,39 +0,0 @@
package single
import (
ui "github.com/gizak/termui"
"regexp"
"strings"
)
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(allEnvs string) {
envs := strings.Split(allEnvs, ";")
w.Rows = [][]string{}
for _, env := range envs {
match := envPattern.FindStringSubmatch(env)
key := match[1]
value := match[2]
w.data[key] = value
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
}
w.Height = len(w.Rows) + 2
}

View File

@@ -1,57 +0,0 @@
package single
import (
"strings"
ui "github.com/gizak/termui"
)
var displayInfo = []string{"id", "name", "image", "ports", "IPs", "state", "created", "uptime", "health"}
type Info struct {
*ui.Table
data map[string]string
}
func NewInfo() *Info {
p := ui.NewTable()
p.Height = 4
p.Width = colWidth[0]
p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false
i := &Info{p, make(map[string]string)}
return i
}
func (w *Info) Set(k, v string) {
w.data[k] = v
// rebuild rows
w.Rows = [][]string{}
for _, k := range displayInfo {
if v, ok := w.data[k]; ok {
w.Rows = append(w.Rows, mkInfoRows(k, v)...)
}
}
w.Height = len(w.Rows) + 2
}
// Build row(s) from a key and value string
func mkInfoRows(k, v string) (rows [][]string) {
lines := strings.Split(v, "\n")
// initial row with field name
rows = append(rows, []string{k, lines[0]})
// append any additional lines in separate row
if len(lines) > 1 {
for _, line := range lines[1:] {
if line != "" {
rows = append(rows, []string{"", line})
}
}
}
return rows
}

View File

@@ -1,51 +0,0 @@
package single
import (
"fmt"
"strings"
"github.com/bcicen/ctop/cwidgets"
ui "github.com/gizak/termui"
)
type IO struct {
*ui.Sparklines
readHist *DiffHist
writeHist *DiffHist
}
func NewIO() *IO {
io := &IO{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}
io.BorderLabel = "IO"
io.Height = 6
io.Width = colWidth[0]
io.X = 0
io.Y = 24
read := ui.NewSparkline()
read.Title = "READ"
read.Height = 1
read.Data = io.readHist.Data
read.LineColor = ui.ColorGreen
write := ui.NewSparkline()
write.Title = "WRITE"
write.Height = 1
write.Data = io.writeHist.Data
write.LineColor = ui.ColorYellow
io.Lines = []ui.Sparkline{read, write}
return io
}
func (w *IO) Update(read int64, write int64) {
var rate string
w.readHist.Append(int(read))
rate = strings.ToLower(cwidgets.ByteFormatShort(w.readHist.Val))
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
w.writeHist.Append(int(write))
rate = strings.ToLower(cwidgets.ByteFormatShort(w.writeHist.Val))
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
}

View File

@@ -1,83 +0,0 @@
package single
import (
"time"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
type LogLines struct {
ts []time.Time
data []string
}
func NewLogLines(max int) *LogLines {
ll := &LogLines{
ts: make([]time.Time, max),
data: make([]string, max),
}
return ll
}
func (ll *LogLines) tail(n int) []string {
lines := make([]string, n)
for i := 0; i < n; i++ {
lines = append(lines, ll.data[len(ll.data)-i])
}
return lines
}
func (ll *LogLines) getLines(start, end int) []string {
if end < 0 {
return ll.data[start:]
}
return ll.data[start:end]
}
func (ll *LogLines) add(l models.Log) {
if len(ll.data) == cap(ll.data) {
ll.data = append(ll.data[:0], ll.data[1:]...)
ll.ts = append(ll.ts[:0], ll.ts[1:]...)
}
ll.ts = append(ll.ts, l.Timestamp)
ll.data = append(ll.data, l.Message)
log.Debugf("recorded log line: %v", l)
}
type Logs struct {
*ui.List
lines *LogLines
}
func NewLogs(stream chan models.Log) *Logs {
p := ui.NewList()
p.Y = ui.TermHeight() / 2
p.X = 0
p.Height = ui.TermHeight() - p.Y
p.Width = ui.TermWidth()
//p.Overflow = "wrap"
p.ItemFgColor = ui.ThemeAttr("par.text.fg")
i := &Logs{p, NewLogLines(4098)}
go func() {
for line := range stream {
i.lines.add(line)
ui.Render(i)
}
}()
return i
}
func (w *Logs) Align() {
w.X = colWidth[0]
w.List.Align()
}
func (w *Logs) Buffer() ui.Buffer {
maxLines := w.Height - 2
offset := len(w.lines.data) - maxLines
w.Items = w.lines.getLines(offset, -1)
return w.List.Buffer()
}
// number of rows a line will occupy at current panel width
func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }

View File

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

View File

@@ -3,19 +3,11 @@ package main
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"runtime"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var mstats = &runtime.MemStats{}
func logEvent(e ui.Event) { func logEvent(e ui.Event) {
// skip timer events e.g. /timer/1s
if e.From == "timer" {
return
}
var s string var s string
s += fmt.Sprintf("Type=%s", quote(e.Type)) s += fmt.Sprintf("Type=%s", quote(e.Type))
s += fmt.Sprintf(" Path=%s", quote(e.Path)) s += fmt.Sprintf(" Path=%s", quote(e.Path))
@@ -26,24 +18,8 @@ func logEvent(e ui.Event) {
log.Debugf("new event: %s", s) log.Debugf("new event: %s", s)
} }
func runtimeStats() {
var msg string
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
runtime.ReadMemStats(mstats)
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
log.Debugf("runtime: %v", msg)
}
func runtimeStack() {
buf := make([]byte, 32768)
buf = buf[:runtime.Stack(buf, true)]
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
}
// log container, metrics, and widget state // log container, metrics, and widget state
func dumpContainer(c *container.Container) { func dumpContainer(c *Container) {
msg := fmt.Sprintf("logging state for container: %s\n", c.Id) msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
for k, v := range c.Meta { for k, v := range c.Meta {
msg += fmt.Sprintf("Meta.%s = %s\n", k, v) msg += fmt.Sprintf("Meta.%s = %s\n", k, v)

156
dockersource.go Normal file
View File

@@ -0,0 +1,156 @@
package main
import (
"sort"
"strings"
"sync"
"github.com/bcicen/ctop/metrics"
"github.com/fsouza/go-dockerclient"
)
type ContainerSource interface {
All() Containers
Get(string) (*Container, bool)
}
type DockerContainerSource struct {
client *docker.Client
containers map[string]*Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewDockerContainerSource() *DockerContainerSource {
// init docker client
client, err := docker.NewClientFromEnv()
if err != nil {
panic(err)
}
cm := &DockerContainerSource{
client: client,
containers: make(map[string]*Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
}
go cm.Loop()
cm.refreshAll()
go cm.watchEvents()
return cm
}
// Docker events watcher
func (cm *DockerContainerSource) watchEvents() {
log.Info("docker event listener starting")
events := make(chan *docker.APIEvents)
cm.client.AddEventListener(events)
for e := range events {
if e.Type != "container" {
continue
}
switch e.Action {
case "start", "die", "pause", "unpause":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
cm.needsRefresh <- e.ID
case "destroy":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
cm.delByID(e.ID)
}
}
}
func (cm *DockerContainerSource) refresh(c *Container) {
insp := cm.inspect(c.Id)
// remove container if no longer exists
if insp == nil {
cm.delByID(c.Id)
return
}
c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image)
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetState(insp.State.Status)
}
func (cm *DockerContainerSource) inspect(id string) *docker.Container {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, ok := err.(*docker.NoSuchContainer); ok == false {
log.Errorf(err.Error())
}
}
return c
}
// Mark all container IDs for refresh
func (cm *DockerContainerSource) refreshAll() {
opts := docker.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
panic(err)
}
for _, i := range allContainers {
c := cm.MustGet(i.ID)
c.SetMeta("name", shortName(i.Names[0]))
c.SetState(i.State)
cm.needsRefresh <- c.Id
}
}
func (cm *DockerContainerSource) Loop() {
for id := range cm.needsRefresh {
c := cm.MustGet(id)
cm.refresh(c)
}
}
// Get a single container, creating one anew if not existing
func (cm *DockerContainerSource) MustGet(id string) *Container {
c, ok := cm.Get(id)
// append container struct for new containers
if !ok {
// create collector
collector := metrics.NewDocker(cm.client, id)
// create container
c = NewContainer(id, collector)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
}
return c
}
// Get a single container, by ID
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
cm.lock.Unlock()
return c, ok
}
// Remove containers by ID
func (cm *DockerContainerSource) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
func (cm *DockerContainerSource) All() (containers Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
cm.lock.Unlock()
sort.Sort(containers)
containers.Filter()
return containers
}
// use primary container name
func shortName(name string) string {
return strings.Replace(name, "/", "", 1)
}

85
glide.lock generated Normal file
View File

@@ -0,0 +1,85 @@
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
updated: 2017-03-12T09:53:35.212073637+07:00
imports:
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/docker/docker
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
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/pools
- pkg/promise
- pkg/stdcopy
- 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/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/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/opencontainers/runc
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/Sirupsen/logrus
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
- name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
subpackages:
- unix
- windows
testImports: []

11
glide.yaml Normal file
View File

@@ -0,0 +1,11 @@
package: github.com/bcicen/ctop
import:
- package: github.com/fsouza/go-dockerclient
- 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/op/go-logging
version: ^1.0.0

21
go.mod
View File

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

266
go.sum
View File

@@ -1,266 +0,0 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU=
github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 h1:2pI3ZsoefWIi++8EqmANoC7Px/v2lRwnleVUcCuFgLg=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5/go.mod h1:yIA9ITWZD1p4/DvCQ44xvhyVb9XEUlVnY1rzGSHwbiM=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd h1:xqaBnULC8wEnQpRDXAsDgXkU/STqoluz1REOoegSfNU=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/checkpoint-restore/go-criu/v5 v5.0.0 h1:TW8f/UvntYoVDMN1K2HlT82qH1rb0sOjpGw3m6Ym+i4=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
github.com/cilium/ebpf v0.5.0 h1:E1KshmrMEtkMP2UjlWzfmUV1owWY+BnbL5FxxuatnrU=
github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk=
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a h1:jEIoR0aA5GogXZ8pP3DUzE+zrhaF6/1rYZy+7KkYEWM=
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/go-systemd/v22 v22.3.1 h1:7OO2CXWMYNDdaAzP51t4lCCZWwpQHmvPbm9sxWjm3So=
github.com/coreos/go-systemd/v22 v22.3.1/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible h1:lwpV3629md5omgAKjxPWX17shI7vMRpE3nyb9WHn8pA=
github.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsouza/go-dockerclient v1.7.0 h1:Ie1/8pAnBHNyCbSIDnYKBdXUEobk4AeJhWZz7k6rWfc=
github.com/fsouza/go-dockerclient v1.7.0/go.mod h1:Ny0LfP7OOsYu9nAi4339E4Ifor6nGBFO2M8lnd2nR+c=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c h1:/hc+TxW4Q1v6aqNPHE5jiaNF2xEK0CzWTgo25RQhQ+U=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.mod h1:FJRkXmPrkHw0WDjB/LXMUhjWJ112Y6JUYnIVBOy8oH8=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM=
github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyPiK2mtZnPrpDl5UnZ64eCkw=
github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884 h1:fcs71SMqqDhUD+PbpIv9xf3EH9F9s6HfiLwr6jKm1VA=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc95 h1:RMuWVfY3E1ILlVsC3RhIq38n4sJtlOFwU9gfFZSqrd0=
github.com/opencontainers/runc v1.0.0-rc95/go.mod h1:z+bZxa/+Tz/FmYVWkhUajJdzFeOqjc5vrqskhVyHGUM=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc=
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.8.0 h1:+77ba4ar4jsCbL1GLbFL8fFM57w6suPfSS9PDLDY7KM=
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 h1:dofHuld+js7eKSemxqTVIo8yRlpRw+H1SdpzZxWruBc=
github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

181
grid.go
View File

@@ -2,48 +2,10 @@ package main
import ( import (
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/cwidgets/single" "github.com/bcicen/ctop/cwidgets/expanded"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
func ShowConnError(err error) (exit bool) {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
setErr := func(err error) {
errView.Append(err.Error())
errView.Append("attempting to reconnect...")
ui.Render(errView)
}
HandleKeys("exit", func() {
exit = true
ui.StopLoop()
})
ui.Handle("/timer/1s", func(ui.Event) {
_, err := cursor.RefreshContainers()
if err == nil {
ui.StopLoop()
return
}
setErr(err)
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
errView.Resize()
ui.Clear()
ui.Render(errView)
log.Infof("RESIZE")
})
errView.Resize()
setErr(err)
ui.Loop()
return exit
}
func RedrawRows(clr bool) { func RedrawRows(clr bool) {
// reinit body rows // reinit body rows
cGrid.Clear() cGrid.Clear()
@@ -55,7 +17,6 @@ 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 {
@@ -73,108 +34,62 @@ func RedrawRows(clr bool) {
ui.Render(cGrid) ui.Render(cGrid)
} }
func SingleView() MenuFn { func ExpandView(c *Container) {
c := cursor.Selected()
if c == nil {
return nil
}
ui.Clear() ui.Clear()
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
ex := single.NewSingle() ex := expanded.NewExpanded(c.Id)
c.SetUpdater(ex) c.SetUpdater(ex)
ex.Align() ex.Align()
ui.Render(ex) ui.Render(ex)
ui.Handle("/timer/1s", func(ui.Event) {
HandleKeys("up", ex.Up) ui.Render(ex)
HandleKeys("down", ex.Down) })
ui.Handle("/sys/kbd/", func(ui.Event) { ui.StopLoop() })
ui.Handle("/timer/1s", func(ui.Event) { ui.Render(ex) })
ui.Handle("/sys/wnd/resize", func(e ui.Event) { ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ex.SetWidth(ui.TermWidth()) ex.SetWidth(ui.TermWidth())
ex.Align() ex.Align()
log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows()) log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows())
}) })
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop() ui.Loop()
c.SetUpdater(c.Widgets) c.SetUpdater(c.Widgets)
return nil
} }
func RefreshDisplay() error { func RefreshDisplay() {
// skip display refresh during scroll needsClear := cursor.RefreshContainers()
if !cursor.isScrolling {
needsClear, err := cursor.RefreshContainers()
if err != nil {
return err
}
RedrawRows(needsClear) RedrawRows(needsClear)
}
return nil
} }
func Display() bool { func Display() bool {
var menu MenuFn var menu func()
var connErr error 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)
HandleKeys("up", cursor.Up) ui.Handle("/sys/kbd/<up>", func(ui.Event) { cursor.Up() })
HandleKeys("down", cursor.Down) ui.Handle("/sys/kbd/<down>", func(ui.Event) { cursor.Down() })
HandleKeys("pgup", cursor.PgUp)
HandleKeys("pgdown", cursor.PgDown)
HandleKeys("exit", ui.StopLoop)
HandleKeys("help", func() {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
menu = ContainerMenu expand = true
ui.StopLoop()
})
ui.Handle("/sys/kbd/<left>", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<right>", func(ui.Event) {
menu = SingleView
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/e", func(ui.Event) {
menu = ExecShell
ui.StopLoop()
})
ui.Handle("/sys/kbd/w", func(ui.Event) {
menu = OpenInBrowser()
})
ui.Handle("/sys/kbd/o", func(ui.Event) {
menu = SingleView
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
ui.Handle("/sys/kbd/C-c", func(ui.Event) { ui.StopLoop() })
ui.Handle("/sys/kbd/a", func(ui.Event) { ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers") config.Toggle("allContainers")
connErr = RefreshDisplay() RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
}) })
ui.Handle("/sys/kbd/D", func(ui.Event) { ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected()) dumpContainer(cursor.Selected())
@@ -183,6 +98,10 @@ func Display() bool {
menu = FilterMenu menu = FilterMenu
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/h", func(ui.Event) {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/H", func(ui.Event) { ui.Handle("/sys/kbd/H", func(ui.Event) {
config.Toggle("enableHeader") config.Toggle("enableHeader")
RedrawRows(true) RedrawRows(true)
@@ -194,33 +113,13 @@ func Display() bool {
menu = SortMenu menu = SortMenu
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/c", func(ui.Event) {
menu = ColumnsMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/S", func(ui.Event) {
path, err := config.Write()
if err == nil {
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() { RefreshDisplay()
ui.StopLoop()
}
connErr = RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
}) })
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())
@@ -228,28 +127,16 @@ func Display() bool {
}) })
ui.Loop() ui.Loop()
if connErr != nil {
return ShowConnError(connErr)
}
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 {
for menu != nil { menu()
menu = menu() return false
}
if expand {
c := cursor.Selected()
if c != nil {
ExpandView(c)
} }
return false return false
} }
return true return true
} }

View File

@@ -1,84 +0,0 @@
#!/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!"

41
keys.go
View File

@@ -1,41 +0,0 @@
package main
import (
ui "github.com/gizak/termui"
)
// Common action keybindings
var keyMap = map[string][]string{
"up": []string{
"/sys/kbd/<up>",
"/sys/kbd/k",
},
"down": []string{
"/sys/kbd/<down>",
"/sys/kbd/j",
},
"pgup": []string{
"/sys/kbd/<previous>",
"/sys/kbd/C-<up>",
},
"pgdown": []string{
"/sys/kbd/<next>",
"/sys/kbd/C-<down>",
},
"exit": []string{
"/sys/kbd/q",
"/sys/kbd/C-c",
"/sys/kbd/<escape>",
},
"help": []string{
"/sys/kbd/h",
"/sys/kbd/?",
},
}
// Apply a common handler function to all given keys
func HandleKeys(i string, f func()) {
for _, k := range keyMap[i] {
ui.Handle(k, func(ui.Event) { f() })
}
}

View File

@@ -1,8 +1,6 @@
package logging package logging
import ( import (
"fmt"
"os"
"time" "time"
"github.com/op/go-logging" "github.com/op/go-logging"
@@ -15,43 +13,17 @@ const (
var ( var (
Log *CTopLogger Log *CTopLogger
exited bool exited bool
level = logging.INFO // default level level = logging.INFO
format = logging.MustStringFormatter( format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, `%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
) )
) )
type statusMsg struct {
Text string
IsError bool
}
type CTopLogger struct { type CTopLogger struct {
*logging.Logger *logging.Logger
backend *logging.MemoryBackend backend *logging.MemoryBackend
logFile *os.File
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
@@ -59,37 +31,12 @@ func Init() *CTopLogger {
Log = &CTopLogger{ Log = &CTopLogger{
logging.MustGetLogger("ctop"), logging.MustGetLogger("ctop"),
logging.NewMemoryBackend(size), logging.NewMemoryBackend(size),
nil,
[]statusMsg{},
} }
debugMode := debugMode()
if debugMode {
level = logging.DEBUG
}
backendLvl := logging.AddModuleLevel(Log.backend) backendLvl := logging.AddModuleLevel(Log.backend)
backendLvl.SetLevel(level, "") backendLvl.SetLevel(level, "")
logFilePath := debugModeFile()
if logFilePath == "" {
logging.SetBackend(backendLvl) logging.SetBackend(backendLvl)
} else {
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
logging.SetBackend(backendLvl)
Log.Error("Unable to create log file: %s", err.Error())
} else {
backendFile := logging.NewLogBackend(logFile, "", 0)
backendFileLvl := logging.AddModuleLevel(backendFile)
backendFileLvl.SetLevel(level, "")
logging.SetBackend(backendLvl, backendFileLvl)
Log.logFile = logFile
}
}
if debugMode {
StartServer()
}
Log.Notice("logger initialized") Log.Notice("logger initialized")
} }
return Log return Log
@@ -122,12 +69,5 @@ func (log *CTopLogger) tail() chan string {
func (log *CTopLogger) Exit() { func (log *CTopLogger) Exit() {
exited = true exited = true
if log.logFile != nil {
_ = log.logFile.Close()
}
StopServer() StopServer()
} }
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
func debugModeFile() string { return os.Getenv("CTOP_DEBUG_FILE") }

View File

@@ -2,14 +2,12 @@ package logging
import ( import (
"fmt" "fmt"
"io"
"net" "net"
"sync" "sync"
) )
const ( const (
socketPath = "./ctop.sock" path = "/tmp/ctop.sock"
socketAddr = "0.0.0.0:9000"
) )
var server struct { var server struct {
@@ -18,13 +16,7 @@ var server struct {
} }
func getListener() net.Listener { func getListener() net.Listener {
var ln net.Listener ln, err := net.Listen("unix", path)
var err error
if debugModeTCP() {
ln, err = net.Listen("tcp", socketAddr)
} else {
ln, err = net.Listen("unix", socketPath)
}
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -57,13 +49,13 @@ func StopServer() {
} }
} }
func handler(wc io.WriteCloser) { func handler(conn net.Conn) {
server.wg.Add(1) server.wg.Add(1)
defer server.wg.Done() defer server.wg.Done()
defer wc.Close() defer conn.Close()
for msg := range Log.tail() { for msg := range Log.tail() {
msg = fmt.Sprintf("%s\n", msg) msg = fmt.Sprintf("%s\n", msg)
wc.Write([]byte(msg)) conn.Write([]byte(msg))
} }
wc.Write([]byte("bye\n")) conn.Write([]byte("bye\n"))
} }

128
main.go
View File

@@ -1,155 +1,101 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
"runtime"
"strings"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/compact" "github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/widgets" "github.com/bcicen/ctop/widgets"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
tm "github.com/nsf/termbox-go"
) )
var ( var (
build = "none" build = "none"
version = "dev-build" version = "dev-build"
goVersion = runtime.Version()
log *logging.CTopLogger log *logging.CTopLogger
cursor *GridCursor cursor *GridCursor
cGrid *compact.CompactGrid cGrid *compact.CompactGrid
header *widgets.CTopHeader header *widgets.CTopHeader
status *widgets.StatusLine
errView *widgets.ErrorView
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
) )
func main() { func main() {
readArgs()
defer panicExit() defer panicExit()
// parse command line arguments
var (
versionFlag = flag.Bool("v", false, "output version information and exit")
helpFlag = flag.Bool("h", false, "display this help dialog")
filterFlag = flag.String("f", "", "filter containers")
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
sortFieldFlag = flag.String("s", "", "select container sort field")
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
invertFlag = flag.Bool("i", false, "invert default colors")
connectorFlag = flag.String("connector", "docker", "container connector to use")
)
flag.Parse()
if *versionFlag {
fmt.Println(versionStr)
os.Exit(0)
}
if *helpFlag {
printHelp()
os.Exit(0)
}
// init logger
log = logging.Init()
// init global config and read config file if exists
config.Init()
if err := config.Read(); err != nil {
log.Warningf("reading config: %s", err)
}
// override default config values with command line flags
if *filterFlag != "" {
config.Update("filterStr", *filterFlag)
}
if *activeOnlyFlag {
config.Toggle("allContainers")
}
if *sortFieldFlag != "" {
validSort(*sortFieldFlag)
config.Update("sortField", *sortFieldFlag)
}
if *reverseSortFlag {
config.Toggle("sortReversed")
}
// init ui // init ui
if *invertFlag {
InvertColorMap()
}
ui.ColorMap = ColorMap // override default colormap ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
panic(err) panic(err)
} }
tm.SetInputMode(tm.InputAlt) defer ui.Close()
defer Shutdown() // init global config
// init grid, cursor, header config.Init()
cSuper, err := connector.ByName(*connectorFlag)
if err != nil { // init logger
panic(err) log = logging.Init()
if config.GetSwitchVal("loggingEnabled") {
logging.StartServer()
} }
cursor = &GridCursor{cSuper: cSuper}
// init grid, cursor, header
cursor = NewGridCursor()
cGrid = compact.NewCompactGrid() cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader() header = widgets.NewCTopHeader()
status = widgets.NewStatusLine()
errView = widgets.NewErrorView()
for { for {
exit := Display() exit := Display()
if exit { if exit {
log.Notice("shutting down")
log.Exit()
return return
} }
} }
} }
func Shutdown() { func readArgs() {
log.Notice("shutting down") if len(os.Args) < 2 {
log.Exit() return
if tm.IsInit {
ui.Close()
} }
} for _, arg := range os.Args[1:] {
switch arg {
// ensure a given sort field is valid case "-v", "version":
func validSort(s string) { printVersion()
if _, ok := container.Sorters[s]; !ok { os.Exit(0)
fmt.Printf("invalid sort field: %s\n", s) case "-h", "help":
printHelp()
os.Exit(0)
default:
fmt.Printf("invalid option or argument: \"%s\"\n", arg)
os.Exit(1) os.Exit(1)
} }
}
} }
func panicExit() { func panicExit() {
if r := recover(); r != nil { if r := recover(); r != nil {
Shutdown() ui.Clear()
panic(r) fmt.Printf("panic: %s\n", r)
fmt.Printf("error: %s\n", r)
os.Exit(1) os.Exit(1)
} }
} }
var helpMsg = `ctop - interactive container viewer var helpMsg = `ctop - container metric viewer
usage: ctop [options] usage: ctop [options]
options: options:
-h display this help dialog
-v output version information and exit
` `
func printHelp() { func printHelp() {
fmt.Println(helpMsg) fmt.Println(helpMsg)
flag.PrintDefaults() }
fmt.Printf("\navailable connectors: ")
fmt.Println(strings.Join(connector.Enabled(), ", ")) func printVersion() {
fmt.Printf("ctop version %v, build %v\n", version, build)
} }

436
menus.go
View File

@@ -1,40 +1,23 @@
package main package main
import ( import (
"fmt"
"strings"
"time"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/widgets" "github.com/bcicen/ctop/widgets"
"github.com/bcicen/ctop/widgets/menu" "github.com/bcicen/ctop/widgets/menu"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
"github.com/pkg/browser"
) )
// MenuFn executes a menu window, returning the next menu or nil
type MenuFn func() MenuFn
var helpDialog = []menu.Item{ var helpDialog = []menu.Item{
{"<enter> - open container menu", ""}, menu.Item{"[a] - toggle display of all containers", ""},
{"", ""}, menu.Item{"[f] - filter displayed containers", ""},
{"[a] - toggle display of all containers", ""}, menu.Item{"[h] - open this help dialog", ""},
{"[f] - filter displayed containers", ""}, menu.Item{"[H] - toggle ctop header", ""},
{"[h] - open this help dialog", ""}, menu.Item{"[s] - select container sort field", ""},
{"[H] - toggle ctop header", ""}, menu.Item{"[r] - reverse container sort order", ""},
{"[s] - select container sort field", ""}, menu.Item{"[q] - exit ctop", ""},
{"[r] - reverse container sort order", ""},
{"[o] - open single view", ""},
{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
{"[e] - exec shell", ""},
{"[w] - open browser (first port is http)", ""},
{"[c] - configure columns", ""},
{"[S] - save current configuration to file", ""},
{"[q] - exit ctop", ""},
} }
func HelpMenu() MenuFn { func HelpMenu() {
ui.Clear() ui.Clear()
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
@@ -42,25 +25,20 @@ func HelpMenu() MenuFn {
m := menu.NewMenu() m := menu.NewMenu()
m.BorderLabel = "Help" m.BorderLabel = "Help"
m.AddItems(helpDialog...) m.AddItems(helpDialog...)
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Clear()
ui.Render(m) ui.Render(m)
})
ui.Handle("/sys/kbd/", func(ui.Event) { ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop() ui.StopLoop()
}) })
ui.Loop() ui.Loop()
return nil
} }
func FilterMenu() MenuFn { func FilterMenu() {
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
i := widgets.NewInput() i := widgets.NewInput()
i.BorderLabel = "Filter" i.BorderLabel = "Filter"
i.SetY(ui.TermHeight() - i.Height) i.SetY(ui.TermHeight() - i.Height)
i.Data = config.GetVal("filterStr")
ui.Render(i) ui.Render(i)
// refresh container rows on input // refresh container rows on input
@@ -74,19 +52,14 @@ func FilterMenu() MenuFn {
}() }()
i.InputHandlers() i.InputHandlers()
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
config.Update("filterStr", "")
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("filterStr", i.Data) config.Update("filterStr", i.Data)
ui.StopLoop() ui.StopLoop()
}) })
ui.Loop() ui.Loop()
return nil
} }
func SortMenu() MenuFn { func SortMenu() {
ui.Clear() ui.Clear()
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
@@ -96,399 +69,18 @@ func SortMenu() MenuFn {
m.SortItems = true m.SortItems = true
m.BorderLabel = "Sort Field" m.BorderLabel = "Sort Field"
for _, field := range container.SortFields() { for _, field := range SortFields() {
m.AddItems(menu.Item{field, ""}) m.AddItems(menu.Item{field, ""})
} }
// set cursor position to current sort field // set cursor position to current sort field
m.SetCursor(config.GetVal("sortField")) m.SetCursor(config.GetVal("sortField"))
HandleKeys("up", m.Up) ui.Render(m)
HandleKeys("down", m.Down) m.NavigationHandlers()
HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("sortField", m.SelectedValue()) config.Update("sortField", m.SelectedItem().Val)
ui.StopLoop()
})
ui.Render(m)
ui.Loop()
return nil
}
func ColumnsMenu() MenuFn {
const (
enabledStr = "[X]"
disabledStr = "[ ]"
padding = 2
)
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.SortItems = false
m.BorderLabel = "Columns"
m.SubText = "Re-order: <Page Up> / <Page Down>"
rebuild := func() {
// get padding for right alignment of enabled status
var maxLen int
for _, col := range config.GlobalColumns {
if len(col.Label) > maxLen {
maxLen = len(col.Label)
}
}
maxLen += padding
// rebuild menu items
m.ClearItems()
for _, col := range config.GlobalColumns {
txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label))
if col.Enabled {
txt += enabledStr
} else {
txt += disabledStr
}
m.AddItems(menu.Item{col.Name, txt})
}
}
upFn := func() {
config.ColumnLeft(m.SelectedValue())
m.Up()
rebuild()
}
downFn := func() {
config.ColumnRight(m.SelectedValue())
m.Down()
rebuild()
}
toggleFn := func() {
config.ColumnToggle(m.SelectedValue())
rebuild()
}
rebuild()
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("enter", toggleFn)
HandleKeys("pgup", upFn)
HandleKeys("pgdown", downFn)
ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { toggleFn() })
HandleKeys("exit", func() {
cSource, err := cursor.cSuper.Get()
if err == nil {
for _, c := range cSource.All() {
c.RecreateWidgets()
}
}
ui.StopLoop()
})
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: "[o] single view"},
menu.Item{Val: "logs", Label: "[l] log view"},
}
if c.Meta["state"] == "running" {
items = append(items, menu.Item{Val: "stop", Label: "[s] stop"})
items = append(items, menu.Item{Val: "pause", Label: "[p] pause"})
items = append(items, menu.Item{Val: "restart", Label: "[r] restart"})
items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"})
if c.Meta["Web Port"] != "" {
items = append(items, menu.Item{Val: "browser", Label: "[w] open in browser"})
}
}
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
items = append(items, menu.Item{Val: "start", Label: "[s] start"})
items = append(items, menu.Item{Val: "remove", Label: "[R] remove"})
}
if c.Meta["state"] == "paused" {
items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"})
}
items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"})
m.AddItems(items...)
ui.Render(m)
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
var selected string
// shortcuts
ui.Handle("/sys/kbd/o", func(ui.Event) {
selected = "single"
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
selected = "logs"
ui.StopLoop()
})
if c.Meta["state"] != "paused" {
ui.Handle("/sys/kbd/s", func(ui.Event) {
if c.Meta["state"] == "running" {
selected = "stop"
} else {
selected = "start"
}
ui.StopLoop()
})
}
if c.Meta["state"] != "exited" && c.Meta["state"] != "created" {
ui.Handle("/sys/kbd/p", func(ui.Event) {
if c.Meta["state"] == "paused" {
selected = "unpause"
} else {
selected = "pause"
}
ui.StopLoop()
})
}
if c.Meta["state"] == "running" {
ui.Handle("/sys/kbd/e", func(ui.Event) {
selected = "exec"
ui.StopLoop()
})
ui.Handle("/sys/kbd/r", func(ui.Event) {
selected = "restart"
ui.StopLoop()
})
if c.Meta["Web Port"] != "" {
ui.Handle("/sys/kbd/w", func(ui.Event) {
selected = "browser"
})
}
}
ui.Handle("/sys/kbd/R", func(ui.Event) {
selected = "remove"
ui.StopLoop()
})
ui.Handle("/sys/kbd/c", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
selected = m.SelectedValue()
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop() ui.StopLoop()
}) })
ui.Loop() ui.Loop()
var nextMenu MenuFn
switch selected {
case "single":
nextMenu = SingleView
case "logs":
nextMenu = LogMenu
case "exec":
nextMenu = ExecShell
case "browser":
nextMenu = OpenInBrowser
case "start":
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
case "stop":
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
case "remove":
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
case "pause":
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
case "unpause":
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
return nextMenu
} }
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
}
func ExecShell() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
// Detect and execute default shell in container.
// Execute Ash shell command: /bin/sh -c
// Reset colors: printf '\e[0m\e[?25h'
// Clear screen
// Run default shell for the user. It's configured in /etc/passwd and looks like root:x:0:0:root:/root:/bin/bash:
// 1. Get current user id: id -un
// 2. Find user's line in /etc/passwd by grep
// 3. Extract default user's shell by cutting seven's column separated by :
// 4. Execute the shell path with eval
if err := c.Exec([]string{"/bin/sh", "-c", "printf '\\e[0m\\e[?25h' && clear && eval `grep ^$(id -un): /etc/passwd | cut -d : -f 7-`"}); err != nil {
log.StatusErr(err)
}
return nil
}
func OpenInBrowser() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
webPort := c.Meta.Get("Web Port")
if webPort == "" {
return nil
}
link := "http://" + webPort + "/"
browser.OpenURL(link)
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.SelectedValue() {
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

@@ -1,17 +1,16 @@
package collector package metrics
import ( import (
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient" api "github.com/fsouza/go-dockerclient"
) )
// Docker collector // Docker collector
type Docker struct { type Docker struct {
models.Metrics Metrics
id string id string
client *api.Client client *api.Client
running bool running bool
stream chan models.Metrics stream chan Metrics
done chan bool done chan bool
lastCpu float64 lastCpu float64
lastSysCpu float64 lastSysCpu float64
@@ -19,7 +18,7 @@ type Docker struct {
func NewDocker(client *api.Client, id string) *Docker { func NewDocker(client *api.Client, id string) *Docker {
return &Docker{ return &Docker{
Metrics: models.Metrics{}, Metrics: Metrics{},
id: id, id: id,
client: client, client: client,
} }
@@ -27,7 +26,7 @@ func NewDocker(client *api.Client, id string) *Docker {
func (c *Docker) Start() { func (c *Docker) Start() {
c.done = make(chan bool) c.done = make(chan bool)
c.stream = make(chan models.Metrics) c.stream = make(chan Metrics)
stats := make(chan *api.Stats) stats := make(chan *api.Stats)
go func() { go func() {
@@ -47,7 +46,6 @@ func (c *Docker) Start() {
c.ReadCPU(s) c.ReadCPU(s)
c.ReadMem(s) c.ReadMem(s)
c.ReadNet(s) c.ReadNet(s)
c.ReadIO(s)
c.stream <- c.Metrics c.stream <- c.Metrics
} }
log.Infof("collector stopped for container: %s", c.id) log.Infof("collector stopped for container: %s", c.id)
@@ -61,42 +59,32 @@ func (c *Docker) Running() bool {
return c.running return c.running
} }
func (c *Docker) Stream() chan models.Metrics { func (c *Docker) Stream() chan Metrics {
return c.stream return c.stream
} }
func (c *Docker) Logs() LogCollector {
return NewDockerLogs(c.id, c.client)
}
// Stop collector // Stop collector
func (c *Docker) Stop() { func (c *Docker) Stop() {
c.running = false
c.done <- true c.done <- true
} }
func (c *Docker) ReadCPU(stats *api.Stats) { func (c *Docker) ReadCPU(stats *api.Stats) {
ncpus := uint8(stats.CPUStats.OnlineCPUs) ncpus := float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
if ncpus == 0 {
ncpus = uint8(len(stats.CPUStats.CPUUsage.PercpuUsage))
}
total := float64(stats.CPUStats.CPUUsage.TotalUsage) total := float64(stats.CPUStats.CPUUsage.TotalUsage)
system := float64(stats.CPUStats.SystemCPUUsage) system := float64(stats.CPUStats.SystemCPUUsage)
cpudiff := total - c.lastCpu cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu syscpudiff := system - c.lastSysCpu
c.NCpus = ncpus c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
c.CPUUtil = percent(cpudiff, syscpudiff)
c.lastCpu = total c.lastCpu = total
c.lastSysCpu = system c.lastSysCpu = system
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 - stats.MemoryStats.Stats.Cache) c.MemUsage = int64(stats.MemoryStats.Usage)
c.MemLimit = int64(stats.MemoryStats.Limit) c.MemLimit = int64(stats.MemoryStats.Limit)
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit)) c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
} }
func (c *Docker) ReadNet(stats *api.Stats) { func (c *Docker) ReadNet(stats *api.Stats) {
@@ -107,16 +95,3 @@ func (c *Docker) ReadNet(stats *api.Stats) {
} }
c.NetRx, c.NetTx = rx, tx c.NetRx, c.NetTx = rx, tx
} }
func (c *Docker) ReadIO(stats *api.Stats) {
var read, write int64
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
if blk.Op == "Read" {
read += int64(blk.Value)
}
if blk.Op == "Write" {
write += int64(blk.Value)
}
}
c.IOBytesRead, c.IOBytesWrite = read, write
}

39
metrics/main.go Normal file
View File

@@ -0,0 +1,39 @@
package metrics
import (
"math"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
type Metrics struct {
CPUUtil int
NetTx int64
NetRx int64
MemLimit int64
MemPercent int
MemUsage int64
}
func NewMetrics() Metrics {
return Metrics{
CPUUtil: -1,
NetTx: -1,
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
}
}
type Collector interface {
Stream() chan Metrics
Running() bool
Start()
Stop()
}
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}

View File

@@ -1,18 +1,16 @@
// +build !release // +build !release
package collector package metrics
import ( import (
"math/rand" "math/rand"
"time" "time"
"github.com/bcicen/ctop/models"
) )
// Mock collector // Mock collector
type Mock struct { type Mock struct {
models.Metrics Metrics
stream chan models.Metrics stream chan Metrics
done bool done bool
running bool running bool
aggression int64 aggression int64
@@ -20,7 +18,7 @@ type Mock struct {
func NewMock(a int64) *Mock { func NewMock(a int64) *Mock {
c := &Mock{ c := &Mock{
Metrics: models.Metrics{}, Metrics: Metrics{},
aggression: a, aggression: a,
} }
c.MemLimit = 2147483648 c.MemLimit = 2147483648
@@ -33,33 +31,23 @@ func (c *Mock) Running() bool {
func (c *Mock) Start() { func (c *Mock) Start() {
c.done = false c.done = false
c.stream = make(chan models.Metrics) c.stream = make(chan Metrics)
go c.run() go c.run()
} }
func (c *Mock) Stop() { func (c *Mock) Stop() {
c.running = false
c.done = true c.done = true
} }
func (c *Mock) Stream() chan models.Metrics { func (c *Mock) Stream() chan Metrics {
return c.stream return c.stream
} }
func (c *Mock) Logs() LogCollector {
return &MockLogs{make(chan bool)}
}
func (c *Mock) run() { func (c *Mock) run() {
c.running = true c.running = true
rand.Seed(int64(time.Now().Nanosecond())) rand.Seed(int64(time.Now().Nanosecond()))
defer close(c.stream) defer close(c.stream)
// set to random static value, once
c.Pids = rand.Intn(12)
c.IOBytesRead = rand.Int63n(8098) * c.aggression
c.IOBytesWrite = rand.Int63n(8098) * c.aggression
for { for {
c.CPUUtil += rand.Intn(2) * int(c.aggression) c.CPUUtil += rand.Intn(2) * int(c.aggression)
if c.CPUUtil >= 100 { if c.CPUUtil >= 100 {
@@ -72,7 +60,7 @@ func (c *Mock) run() {
if c.MemUsage > c.MemLimit { if c.MemUsage > c.MemLimit {
c.MemUsage = 0 c.MemUsage = 0
} }
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit)) c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
c.stream <- c.Metrics c.stream <- c.Metrics
if c.done { if c.done {
break break

View File

@@ -1,81 +1,52 @@
// +build !release // +build !release
package connector package main
import ( import (
"math/rand" "math/rand"
"sort"
"strings" "strings"
"time" "time"
"github.com/bcicen/ctop/connector/collector" "github.com/bcicen/ctop/metrics"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator" "github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid" "github.com/nu7hatch/gouuid"
) )
func init() { enabled["mock"] = NewMock } type MockContainerSource struct {
containers Containers
type Mock struct {
containers container.Containers
} }
func NewMock() (Connector, error) { func NewMockContainerSource() *MockContainerSource {
cs := &Mock{} cs := &MockContainerSource{}
go cs.Init() go cs.Init()
go cs.Loop() go cs.Loop()
return cs, nil return cs
} }
// Create Mock containers // Create Mock containers
func (cs *Mock) Init() { func (cs *MockContainerSource) Init() {
rand.Seed(int64(time.Now().Nanosecond())) rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
cs.makeContainer(3, true) cs.makeContainer(3)
} }
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
cs.makeContainer(1, false) cs.makeContainer(1)
} }
} }
func (cs *Mock) Wait() struct{} { func (cs *MockContainerSource) makeContainer(aggression int64) {
ch := make(chan struct{}) collector := metrics.NewMock(aggression)
go func() { c := NewContainer(makeID(), collector)
time.Sleep(30 * time.Second)
close(ch)
}()
return <-ch
}
var healthStates = []string{"starting", "healthy", "unhealthy"}
func (cs *Mock) makeContainer(aggression int64, health bool) {
collector := collector.NewMock(aggression)
manager := manager.NewMock()
c := container.New(makeID(), collector, manager)
c.SetMeta("name", makeName()) c.SetMeta("name", makeName())
c.SetState(makeState()) c.SetState(makeState())
if health {
var i int
c.SetMeta("health", healthStates[i])
go func() {
for {
i++
if i >= len(healthStates) {
i = 0
}
c.SetMeta("health", healthStates[i])
time.Sleep(12 * time.Second)
}
}()
}
cs.containers = append(cs.containers, c) cs.containers = append(cs.containers, c)
} }
func (cs *Mock) Loop() { func (cs *MockContainerSource) Loop() {
iter := 0 iter := 0
for { for {
// Change state for random container // Change state for random container
@@ -89,7 +60,7 @@ func (cs *Mock) Loop() {
} }
// Get a single container, by ID // Get a single container, by ID
func (cs *Mock) Get(id string) (*container.Container, bool) { func (cs *MockContainerSource) Get(id string) (*Container, bool) {
for _, c := range cs.containers { for _, c := range cs.containers {
if c.Id == id { if c.Id == id {
return c, true return c, true
@@ -98,15 +69,15 @@ func (cs *Mock) Get(id string) (*container.Container, bool) {
return nil, false return nil, false
} }
// All returns array of all containers, sorted by field // Return array of all containers, sorted by field
func (cs *Mock) All() container.Containers { func (cs *MockContainerSource) All() Containers {
cs.containers.Sort() sort.Sort(cs.containers)
cs.containers.Filter() cs.containers.Filter()
return cs.containers return cs.containers
} }
// Remove containers by ID // Remove containers by ID
func (cs *Mock) delByID(id string) { func (cs *MockContainerSource) delByID(id string) {
for n, c := range cs.containers { for n, c := range cs.containers {
if c.Id == id { if c.Id == id {
cs.del(n) cs.del(n)
@@ -116,7 +87,7 @@ func (cs *Mock) delByID(id string) {
} }
// Remove one or more containers by index // Remove one or more containers by index
func (cs *Mock) del(idx ...int) { func (cs *MockContainerSource) del(idx ...int) {
for _, i := range idx { for _, i := range idx {
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...) cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
} }

View File

@@ -1,57 +0,0 @@
package models
import "time"
type Log struct {
Timestamp time.Time
Message string
}
type Meta map[string]string
// NewMeta returns an initialized Meta map.
// An optional series of key, values may be provided to populate the map prior to returning
func NewMeta(kvs ...string) Meta {
m := make(Meta)
var i int
for i < len(kvs)-1 {
m[kvs[i]] = kvs[i+1]
i += 2
}
return m
}
func (m Meta) Get(k string) string {
if s, ok := m[k]; ok {
return s
}
return ""
}
type Metrics struct {
NCpus uint8
CPUUtil int
NetTx int64
NetRx int64
MemLimit int64
MemPercent int
MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
}
func NewMetrics() Metrics {
return Metrics{
CPUUtil: -1,
NetTx: -1,
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
}
}

View File

@@ -1,9 +1,8 @@
package container package main
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
) )
@@ -54,22 +53,6 @@ var Sorters = map[string]sortMethod{
} }
return sum1 > sum2 return sum1 > sum2
}, },
"pids": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
if c1.Pids == c2.Pids {
return nameSorter(c1, c2)
}
return c1.Pids > c2.Pids
},
"io": func(c1, c2 *Container) bool {
sum1 := sumIO(c1)
sum2 := sumIO(c2)
// Use secondary sort method if equal values
if sum1 == sum2 {
return nameSorter(c1, c2)
}
return sum1 > sum2
},
"state": func(c1, c2 *Container) bool { "state": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values // Use secondary sort method if equal values
c1state := c1.GetMeta("state") c1state := c1.GetMeta("state")
@@ -79,15 +62,6 @@ var Sorters = map[string]sortMethod{
} }
return stateMap[c1state] > stateMap[c2state] return stateMap[c1state] > stateMap[c2state]
}, },
"uptime": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
c1Uptime := c1.GetMeta("uptime")
c2Uptime := c2.GetMeta("uptime")
if c1Uptime == c2Uptime {
return nameSorter(c1, c2)
}
return c1Uptime > c2Uptime
},
} }
func SortFields() (fields []string) { func SortFields() (fields []string) {
@@ -99,7 +73,6 @@ func SortFields() (fields []string) {
type Containers []*Container type Containers []*Container
func (a Containers) Sort() { sort.Sort(a) }
func (a Containers) Len() int { return len(a) } func (a Containers) Len() int { return len(a) }
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Containers) Less(i, j int) bool { func (a Containers) Less(i, j int) bool {
@@ -115,18 +88,16 @@ func (a Containers) Filter() {
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter)) re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a { for _, c := range a {
c.Display = true c.display = true
// Apply name filter // Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil { if re.FindAllString(c.GetMeta("name"), 1) == nil {
c.Display = false c.display = false
} }
// Apply state filter // Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" { if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
c.Display = false c.display = false
} }
} }
} }
func sumNet(c *Container) int64 { return c.NetRx + c.NetTx } func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
func sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import (
) )
var ( var (
input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_." input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
) )
type Padding [2]int // x,y padding type Padding [2]int // x,y padding
@@ -77,7 +77,7 @@ func (i *Input) KeyPress(e ui.Event) {
if len(i.Data) >= i.MaxLen { if len(i.Data) >= i.MaxLen {
return return
} }
if strings.Contains(input_chars, ch) { if strings.Index(input_chars, ch) > -1 {
i.Data += ch i.Data += ch
i.stream <- i.Data i.stream <- i.Data
ui.Render(i) ui.Render(i)

View File

@@ -11,14 +11,12 @@ 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
Selectable bool // whether menu is navigable
SubText string // optional text to display before items
TextFgColor ui.Attribute TextFgColor ui.Attribute
TextBgColor ui.Attribute TextBgColor ui.Attribute
Selectable bool
cursorPos int cursorPos int
items Items items Items
padding Padding padding Padding
toolTip *ToolTip
} }
func NewMenu() *Menu { func NewMenu() *Menu {
@@ -43,7 +41,7 @@ func (m *Menu) AddItems(items ...Item) {
m.refresh() m.refresh()
} }
// DelItem removes menu item by value or label // Remove menu item by value or label
func (m *Menu) DelItem(s string) (success bool) { func (m *Menu) DelItem(s string) (success bool) {
for n, i := range m.items { for n, i := range m.items {
if i.Val == s || i.Label == s { if i.Val == s || i.Label == s {
@@ -56,11 +54,6 @@ func (m *Menu) DelItem(s string) (success bool) {
return success return success
} }
// ClearItems removes all current menu items
func (m *Menu) ClearItems() {
m.items = m.items[:0]
}
// Move cursor to an position by Item value or label // Move cursor to an position by Item value or label
func (m *Menu) SetCursor(s string) (success bool) { func (m *Menu) SetCursor(s string) (success bool) {
for n, i := range m.items { for n, i := range m.items {
@@ -72,36 +65,26 @@ func (m *Menu) SetCursor(s string) (success bool) {
return false return false
} }
// SetToolTip sets an optional tooltip string to show at bottom of screen // Sort menu items(if enabled) and re-calculate window size
func (m *Menu) SetToolTip(lines ...string) { func (m *Menu) refresh() {
m.toolTip = NewToolTip(lines...) if m.SortItems {
sort.Sort(m.items)
}
m.calcSize()
ui.Render(m)
} }
func (m *Menu) SelectedItem() Item { func (m *Menu) SelectedItem() Item {
return m.items[m.cursorPos] return m.items[m.cursorPos]
} }
func (m *Menu) SelectedValue() string {
return m.items[m.cursorPos].Val
}
func (m *Menu) Buffer() ui.Buffer { 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 {
@@ -114,56 +97,42 @@ func (m *Menu) Buffer() ui.Buffer {
} }
} }
if m.toolTip != nil {
buf.Merge(m.toolTip.Buffer())
}
return buf return buf
} }
func (m *Menu) Up() { func (m *Menu) Up(ui.Event) {
if m.cursorPos > 0 { if m.cursorPos > 0 {
m.cursorPos-- m.cursorPos--
ui.Render(m) ui.Render(m)
} }
} }
func (m *Menu) Down() { func (m *Menu) Down(ui.Event) {
if m.cursorPos < (len(m.items) - 1) { if m.cursorPos < (len(m.items) - 1) {
m.cursorPos++ m.cursorPos++
ui.Render(m) ui.Render(m)
} }
} }
// Sort menu items(if enabled) and re-calculate window size // Setup some default handlers for menu navigation
func (m *Menu) refresh() { func (m *Menu) NavigationHandlers() {
if m.SortItems { ui.Handle("/sys/kbd/<up>", m.Up)
sort.Sort(m.items) ui.Handle("/sys/kbd/<down>", m.Down)
} ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
m.calcSize()
ui.Render(m)
} }
// Set width and height based on menu items // Set width and height based on menu items
func (m *Menu) calcSize() { func (m *Menu) calcSize() {
m.Width = 7 // minimum width m.Width = 7 // minimum width
var height int items := m.items
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 = height + (m.padding[1] * 2) m.Height = len(items) + (m.padding[1] * 2)
} }

View File

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

View File

@@ -1,87 +0,0 @@
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
}

View File

@@ -1,125 +0,0 @@
package widgets
import (
ui "github.com/gizak/termui"
"github.com/mattn/go-runewidth"
)
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 + runewidth.RuneWidth(ch)
}
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:]
}
}

View File

@@ -1,35 +0,0 @@
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))
}
}