Repository: bcicen/ctop
Branch: master
Commit: 59f00dd6aaeb
Files: 75
Total size: 161.3 KB
Directory structure:
gitextract_xqko93bg/
├── .circleci/
│ └── config.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── _docs/
│ ├── build.md
│ ├── connectors.md
│ ├── debug.md
│ ├── single.md
│ └── status.md
├── colors.go
├── config/
│ ├── columns.go
│ ├── file.go
│ ├── main.go
│ ├── param.go
│ └── switch.go
├── connector/
│ ├── collector/
│ │ ├── docker.go
│ │ ├── docker_logs.go
│ │ ├── main.go
│ │ ├── mock.go
│ │ ├── mock_logs.go
│ │ ├── proc.go
│ │ └── runc.go
│ ├── docker.go
│ ├── main.go
│ ├── manager/
│ │ ├── docker.go
│ │ ├── main.go
│ │ ├── mock.go
│ │ └── runc.go
│ ├── mock.go
│ └── runc.go
├── container/
│ ├── main.go
│ └── sort.go
├── cursor.go
├── cwidgets/
│ ├── compact/
│ │ ├── column.go
│ │ ├── gauge.go
│ │ ├── grid.go
│ │ ├── header.go
│ │ ├── row.go
│ │ ├── status.go
│ │ ├── text.go
│ │ └── util.go
│ ├── main.go
│ ├── single/
│ │ ├── cpu.go
│ │ ├── env.go
│ │ ├── hist.go
│ │ ├── info.go
│ │ ├── io.go
│ │ ├── logs.go
│ │ ├── main.go
│ │ ├── mem.go
│ │ └── net.go
│ └── util.go
├── debug.go
├── go.mod
├── go.sum
├── grid.go
├── install.sh
├── keys.go
├── logging/
│ ├── main.go
│ └── server.go
├── main.go
├── menus.go
├── models/
│ └── main.go
└── widgets/
├── error.go
├── header.go
├── input.go
├── menu/
│ ├── items.go
│ ├── main.go
│ └── tooltip.go
├── status.go
├── view.go
└── view_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
build:
working_directory: ~/build
docker:
- image: cimg/go:1.18
steps:
- checkout
- setup_remote_docker:
version: 20.10.11
- 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
================================================
FILE: .gitignore
================================================
ctop
.idea
/vendor/
*.log
================================================
FILE: Dockerfile
================================================
FROM quay.io/vektorcloud/go:1.18
RUN apk add --no-cache make
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"]
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 VektorLab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
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
================================================
FILE: README.md
================================================

#
![release][release] ![homebrew][homebrew] ![macports][macports] ![scoop][scoop]
Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:

as well as a [single container view][single_view] for inspecting a specific container.
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
## Install
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
#### Debian/Ubuntu
Maintained by a [third party](https://packages.azlux.fr/)
```bash
sudo apt-get install ca-certificates curl gnupg lsb-release
curl -fsSL https://azlux.fr/repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/azlux-archive-keyring.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian \
$(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/azlux.list >/dev/null
sudo apt-get update
sudo apt-get install docker-ctop
```
#### Arch
```bash
sudo pacman -S ctop
```
_`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)_
#### Linux (Generic)
```bash
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop
```
#### OS X
```bash
brew install ctop
```
or
```bash
sudo port install ctop
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-darwin-amd64
sudo chmod +x /usr/local/bin/ctop
```
#### Windows
`ctop` is available in [scoop](https://scoop.sh/):
```powershell
scoop install ctop
```
#### Docker
```bash
docker run --rm -ti \
--name=ctop \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop:latest
```
## Building
Build steps can be found [here][build].
## Usage
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
### Config file
While running, use `S` to save the current filters, sort field, and other options to a default config path (`~/.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 ` | 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
| Key | Action |
| :----------------------: | ---------------------------------------------------------- |
| <ENTER> | Open container menu |
| a | Toggle display of all (running and non-running) containers |
| f | Filter displayed containers (`esc` to clear when open) |
| H | Toggle ctop header |
| h | Open help dialog |
| s | Select container sort field |
| r | Reverse container sort order |
| o | Open single view |
| l | View container logs (`t` to toggle timestamp when open) |
| e | Exec Shell |
| c | Configure columns |
| S | Save current configuration to file |
| q | Quit ctop |
[build]: _docs/build.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"
[scoop]: https://img.shields.io/scoop/v/ctop?bucket=main "ctop"
## Alternatives
See [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker.
================================================
FILE: VERSION
================================================
0.7.7
================================================
FILE: _docs/build.md
================================================
# 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
```
================================================
FILE: _docs/connectors.md
================================================
# 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
================================================
FILE: _docs/debug.md
================================================
# 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:

================================================
FILE: _docs/single.md
================================================
# Single Container View
ctop provides a rolling, single container view for following metrics

================================================
FILE: _docs/status.md
================================================
# Status Indicator
The `ctop` grid view provides a compact status indicator to convey container state
### Status
Appearance | Description
--- | ---
red | container is stopped
green | container is running
▮▮ | container is paused
### Health
If the container is configured with a health check, a `+` will appear next to the indicator
Appearance | Description
--- | ---
red | health check in failed state
yellow | health check in starting state
green | health check in OK state
================================================
FILE: colors.go
================================================
package main
import (
"regexp"
ui "github.com/gizak/termui"
)
/*
Valid colors:
ui.ColorDefault
ui.ColorBlack
ui.ColorRed
ui.ColorGreen
ui.ColorYellow
ui.ColorBlue
ui.ColorMagenta
ui.ColorCyan
ui.ColorWhite
*/
var ColorMap = map[string]ui.Attribute{
"fg": ui.ColorWhite,
"bg": ui.ColorDefault,
"block.bg": ui.ColorDefault,
"border.bg": ui.ColorDefault,
"border.fg": ui.ColorWhite,
"label.bg": ui.ColorDefault,
"label.fg": ui.ColorGreen,
"menu.text.fg": ui.ColorWhite,
"menu.text.bg": ui.ColorDefault,
"menu.border.fg": ui.ColorCyan,
"menu.label.fg": ui.ColorGreen,
"header.fg": ui.ColorBlack,
"header.bg": ui.ColorWhite,
"gauge.bar.bg": ui.ColorGreen,
"gauge.percent.fg": ui.ColorWhite,
"linechart.axes.fg": ui.ColorDefault,
"linechart.line.fg": ui.ColorGreen,
"mbarchart.bar.bg": ui.ColorGreen,
"mbarchart.num.fg": ui.ColorWhite,
"mbarchart.text.fg": ui.ColorWhite,
"par.text.fg": ui.ColorWhite,
"par.text.bg": ui.ColorDefault,
"par.text.hi": ui.ColorBlack,
"sparkline.line.fg": ui.ColorGreen,
"sparkline.title.fg": ui.ColorWhite,
"status.ok": ui.ColorGreen,
"status.warn": ui.ColorYellow,
"status.danger": ui.ColorRed,
}
func InvertColorMap() {
re := regexp.MustCompile(".*.fg")
for k := range ColorMap {
if re.FindAllString(k, 1) != nil {
ColorMap[k] = ui.ColorBlack
}
}
ColorMap["par.text.hi"] = ui.ColorWhite
}
================================================
FILE: config/columns.go
================================================
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
}
================================================
FILE: config/file.go
================================================
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
}
================================================
FILE: config/main.go
================================================
package config
import (
"fmt"
"os"
"sync"
"github.com/bcicen/ctop/logging"
)
var (
GlobalParams []*Param
GlobalSwitches []*Switch
GlobalColumns []*Column
lock sync.RWMutex
log = logging.Init()
)
func Init() {
for _, p := range defaultParams {
GlobalParams = append(GlobalParams, p)
log.Infof("loaded default config param [%s]: %s", quote(p.Key), quote(p.Val))
}
for _, s := range defaultSwitches {
GlobalSwitches = append(GlobalSwitches, s)
log.Infof("loaded default 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)
}
}
func quote(s string) string {
return fmt.Sprintf("\"%s\"", s)
}
// Return env var value if set, else return defaultVal
func getEnv(key, defaultVal string) string {
val := os.Getenv(key)
if val != "" {
return val
}
return defaultVal
}
================================================
FILE: config/param.go
================================================
package config
// defaults
var defaultParams = []*Param{
&Param{
Key: "filterStr",
Val: "",
Label: "Container Name or ID Filter",
},
&Param{
Key: "sortField",
Val: "state",
Label: "Container Sort Field",
},
&Param{
Key: "columns",
Val: "status,name,id,cpu,mem,net,io,pids,uptime",
Label: "Enabled Columns",
},
}
type Param struct {
Key string
Val string
Label string
}
// Get Param by key
func Get(k string) *Param {
lock.RLock()
defer lock.RUnlock()
for _, p := range GlobalParams {
if p.Key == k {
return p
}
}
return &Param{} // default
}
// GetVal gets Param value by key
func GetVal(k string) string {
return Get(k).Val
}
// Set param value
func Update(k, v string) {
p := Get(k)
log.Noticef("config change [%s]: %s -> %s", k, quote(p.Val), quote(v))
lock.Lock()
defer lock.Unlock()
p.Val = v
// log.Errorf("ignoring update for non-existant parameter: %s", k)
}
================================================
FILE: config/switch.go
================================================
package config
// defaults
var defaultSwitches = []*Switch{
&Switch{
Key: "sortReversed",
Val: false,
Label: "Reverse sort order",
},
&Switch{
Key: "allContainers",
Val: true,
Label: "Show all containers",
},
&Switch{
Key: "fullRowCursor",
Val: true,
Label: "Highlight entire cursor row (vs. name only)",
},
&Switch{
Key: "enableHeader",
Val: true,
Label: "Enable status header",
},
}
type Switch struct {
Key string
Val bool
Label string
}
// GetSwitch returns Switch by key
func GetSwitch(k string) *Switch {
lock.RLock()
defer lock.RUnlock()
for _, sw := range GlobalSwitches {
if sw.Key == k {
return sw
}
}
return &Switch{} // default
}
// GetSwitchVal returns Switch value by key
func GetSwitchVal(k string) bool {
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
func Toggle(k string) {
sw := GetSwitch(k)
lock.Lock()
defer lock.Unlock()
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)
}
================================================
FILE: connector/collector/docker.go
================================================
package collector
import (
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient"
)
// Docker collector
type Docker struct {
models.Metrics
id string
client *api.Client
running bool
stream chan models.Metrics
done chan bool
lastCpu float64
lastSysCpu float64
}
func NewDocker(client *api.Client, id string) *Docker {
return &Docker{
Metrics: models.Metrics{},
id: id,
client: client,
}
}
func (c *Docker) Start() {
c.done = make(chan bool)
c.stream = make(chan models.Metrics)
stats := make(chan *api.Stats)
go func() {
opts := api.StatsOptions{
ID: c.id,
Stats: stats,
Stream: true,
Done: c.done,
}
c.client.Stats(opts)
c.running = false
}()
go func() {
defer close(c.stream)
for s := range stats {
c.ReadCPU(s)
c.ReadMem(s)
c.ReadNet(s)
c.ReadIO(s)
c.stream <- c.Metrics
}
log.Infof("collector stopped for container: %s", c.id)
}()
c.running = true
log.Infof("collector started for container: %s", c.id)
}
func (c *Docker) Running() bool {
return c.running
}
func (c *Docker) Stream() chan models.Metrics {
return c.stream
}
func (c *Docker) Logs() LogCollector {
return NewDockerLogs(c.id, c.client)
}
// Stop collector
func (c *Docker) Stop() {
c.running = false
c.done <- true
}
func (c *Docker) ReadCPU(stats *api.Stats) {
ncpus := uint8(stats.CPUStats.OnlineCPUs)
if ncpus == 0 {
ncpus = uint8(len(stats.CPUStats.CPUUsage.PercpuUsage))
}
total := float64(stats.CPUStats.CPUUsage.TotalUsage)
system := float64(stats.CPUStats.SystemCPUUsage)
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 *Docker) ReadMem(stats *api.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage - stats.MemoryStats.Stats.Cache)
c.MemLimit = int64(stats.MemoryStats.Limit)
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Docker) ReadNet(stats *api.Stats) {
var rx, tx int64
for _, network := range stats.Networks {
rx += int64(network.RxBytes)
tx += int64(network.TxBytes)
}
c.NetRx, c.NetTx = rx, tx
}
func (c *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
}
================================================
FILE: connector/collector/docker_logs.go
================================================
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
}
================================================
FILE: connector/collector/main.go
================================================
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)
}
================================================
FILE: connector/collector/mock.go
================================================
//go:build !release
// +build !release
package collector
import (
"math/rand"
"time"
"github.com/bcicen/ctop/models"
)
// Mock collector
type Mock struct {
models.Metrics
stream chan models.Metrics
done bool
running bool
aggression int64
}
func NewMock(a int64) *Mock {
c := &Mock{
Metrics: models.Metrics{},
aggression: a,
}
c.MemLimit = 2147483648
return c
}
func (c *Mock) Running() bool {
return c.running
}
func (c *Mock) Start() {
c.done = false
c.stream = make(chan models.Metrics)
go c.run()
}
func (c *Mock) Stop() {
c.running = false
c.done = true
}
func (c *Mock) Stream() chan models.Metrics {
return c.stream
}
func (c *Mock) Logs() LogCollector {
return &MockLogs{make(chan bool)}
}
func (c *Mock) run() {
c.running = true
rand.Seed(int64(time.Now().Nanosecond()))
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 {
c.CPUUtil += rand.Intn(2) * int(c.aggression)
if c.CPUUtil >= 100 {
c.CPUUtil = 0
}
c.NetTx += rand.Int63n(60) * c.aggression
c.NetRx += rand.Int63n(60) * c.aggression
c.MemUsage += rand.Int63n(c.MemLimit/512) * c.aggression
if c.MemUsage > c.MemLimit {
c.MemUsage = 0
}
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
c.stream <- c.Metrics
if c.done {
break
}
time.Sleep(1 * time.Second)
}
c.running = false
}
================================================
FILE: connector/collector/mock_logs.go
================================================
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 }
================================================
FILE: connector/collector/proc.go
================================================
//go:build linux
// +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
}
================================================
FILE: connector/collector/runc.go
================================================
//go:build linux
// +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
}
================================================
FILE: connector/docker.go
================================================
package connector
import (
"fmt"
"strings"
"sync"
"time"
"github.com/op/go-logging"
"github.com/hako/durafmt"
"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 02 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() || insp.State.Running {
endTime = time.Now()
}
uptime := endTime.Sub(insp.State.StartedAt)
return durafmt.Parse(uptime).LimitFirstN(1).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, "/")
}
================================================
FILE: connector/main.go
================================================
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)
}
================================================
FILE: connector/manager/docker.go
================================================
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
}
================================================
FILE: connector/manager/main.go
================================================
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
}
================================================
FILE: connector/manager/mock.go
================================================
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
}
================================================
FILE: connector/manager/runc.go
================================================
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
}
================================================
FILE: connector/mock.go
================================================
//go:build !release
// +build !release
package connector
import (
"math/rand"
"strings"
"time"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid"
)
func init() { enabled["mock"] = NewMock }
type Mock struct {
containers container.Containers
}
func NewMock() (Connector, error) {
cs := &Mock{}
go cs.Init()
go cs.Loop()
return cs, nil
}
// Create Mock containers
func (cs *Mock) Init() {
rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < 4; i++ {
cs.makeContainer(3, true)
}
for i := 0; i < 16; i++ {
cs.makeContainer(1, false)
}
}
func (cs *Mock) Wait() struct{} {
ch := make(chan struct{})
go func() {
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.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)
}
func (cs *Mock) Loop() {
iter := 0
for {
// Change state for random container
if iter%5 == 0 && len(cs.containers) > 0 {
randC := cs.containers[rand.Intn(len(cs.containers))]
randC.SetState(makeState())
}
iter++
time.Sleep(3 * time.Second)
}
}
// Get a single container, by ID
func (cs *Mock) Get(id string) (*container.Container, bool) {
for _, c := range cs.containers {
if c.Id == id {
return c, true
}
}
return nil, false
}
// All returns array of all containers, sorted by field
func (cs *Mock) All() container.Containers {
cs.containers.Sort()
cs.containers.Filter()
return cs.containers
}
// Remove containers by ID
func (cs *Mock) delByID(id string) {
for n, c := range cs.containers {
if c.Id == id {
cs.del(n)
return
}
}
}
// Remove one or more containers by index
func (cs *Mock) del(idx ...int) {
for _, i := range idx {
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
}
log.Infof("removed %d dead containers", len(idx))
}
func makeID() string {
u, err := uuid.NewV4()
if err != nil {
panic(err)
}
return strings.Replace(u.String(), "-", "", -1)[:12]
}
func makeName() string {
n, err := codename.Get(codename.Sanitized)
nsp := strings.Split(n, "-")
if len(nsp) > 2 {
n = strings.Join(nsp[:2], "-")
}
if err != nil {
panic(err)
}
return strings.Replace(n, "-", "_", -1)
}
func makeState() string {
switch rand.Intn(10) {
case 0, 1, 2:
return "exited"
case 3:
return "paused"
}
return "running"
}
================================================
FILE: connector/runc.go
================================================
//go:build linux
// +build linux
package connector
import (
"errors"
"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"
)
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 := libcontainer.New(opts.root)
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 errors.Is(err, libcontainer.ErrNotExist) {
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
}
================================================
FILE: container/main.go
================================================
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()
shortID := id
if len(shortID) > 12 {
shortID = shortID[0:12]
}
return &Container{
Metrics: models.NewMetrics(),
Id: id,
Meta: models.NewMeta("id", shortID),
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)
}
================================================
FILE: container/sort.go
================================================
package container
import (
"fmt"
"regexp"
"sort"
"github.com/bcicen/ctop/config"
)
type sortMethod func(c1, c2 *Container) bool
var stateMap = map[string]int{
"running": 3,
"paused": 2,
"exited": 1,
"created": 0,
"": 0,
}
var idSorter = func(c1, c2 *Container) bool { return c1.Id < c2.Id }
var nameSorter = func(c1, c2 *Container) bool { return c1.GetMeta("name") < c2.GetMeta("name") }
var Sorters = map[string]sortMethod{
"id": idSorter,
"name": nameSorter,
"cpu": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
if c1.CPUUtil == c2.CPUUtil {
return nameSorter(c1, c2)
}
return c1.CPUUtil > c2.CPUUtil
},
"mem": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
if c1.MemUsage == c2.MemUsage {
return nameSorter(c1, c2)
}
return c1.MemUsage > c2.MemUsage
},
"mem %": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
if c1.MemPercent == c2.MemPercent {
return nameSorter(c1, c2)
}
return c1.MemPercent > c2.MemPercent
},
"net": func(c1, c2 *Container) bool {
sum1 := sumNet(c1)
sum2 := sumNet(c2)
// Use secondary sort method if equal values
if sum1 == sum2 {
return nameSorter(c1, c2)
}
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 {
// Use secondary sort method if equal values
c1state := c1.GetMeta("state")
c2state := c2.GetMeta("state")
if c1state == c2state {
return nameSorter(c1, c2)
}
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) {
for k := range Sorters {
fields = append(fields, k)
}
return fields
}
type Containers []*Container
func (a Containers) Sort() { sort.Sort(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) Less(i, j int) bool {
f := Sorters[config.GetVal("sortField")]
if config.GetSwitchVal("sortReversed") {
return f(a[j], a[i])
}
return f(a[i], a[j])
}
func (a Containers) Filter() {
filter := config.GetVal("filterStr")
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a {
c.Display = true
// Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil {
c.Display = false
}
// Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
c.Display = false
}
}
}
func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
func sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }
================================================
FILE: cursor.go
================================================
package main
import (
"math"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui"
)
type GridCursor struct {
selectedID string // id of currently selected container
filtered container.Containers
cSuper *connector.ConnectorSuper
isScrolling bool // toggled when actively scrolling
}
func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) Selected() *container.Container {
idx := gc.Idx()
if idx < gc.Len() {
return gc.filtered[idx]
}
return nil
}
// Refresh containers from source, returning whether the quantity of
// containers has changed and any error
func (gc *GridCursor) RefreshContainers() (bool, error) {
oldLen := gc.Len()
gc.filtered = container.Containers{}
cSource, err := gc.cSuper.Get()
if err != nil {
return true, err
}
// filter Containers by display bool
var cursorVisible bool
for _, c := range cSource.All() {
if c.Display {
if c.Id == gc.selectedID {
cursorVisible = true
}
gc.filtered = append(gc.filtered, c)
}
}
if !cursorVisible || gc.selectedID == "" {
gc.Reset()
}
return oldLen != gc.Len(), nil
}
// Set an initial cursor position, if possible
func (gc *GridCursor) Reset() {
cSource, err := gc.cSuper.Get()
if err != nil {
return
}
for _, c := range cSource.All() {
c.Widgets.UnHighlight()
}
if gc.Len() > 0 {
gc.selectedID = gc.filtered[0].Id
gc.filtered[0].Widgets.Highlight()
}
}
// Idx returns current cursor index
func (gc *GridCursor) Idx() int {
for n, c := range gc.filtered {
if c.Id == gc.selectedID {
return n
}
}
gc.Reset()
return 0
}
func (gc *GridCursor) ScrollPage() {
// skip scroll if no need to page
if gc.Len() < cGrid.MaxRows() {
cGrid.Offset = 0
return
}
idx := gc.Idx()
// page down
if idx >= cGrid.Offset+cGrid.MaxRows() {
cGrid.Offset++
cGrid.Align()
}
// page up
if idx < cGrid.Offset {
cGrid.Offset--
cGrid.Align()
}
}
func (gc *GridCursor) Up() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx()
if idx <= 0 { // already at top
return
}
active := gc.filtered[idx]
next := gc.filtered[idx-1]
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
}
func (gc *GridCursor) Down() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom
return
}
active := gc.filtered[idx]
next := gc.filtered[idx+1]
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Highlight()
gc.ScrollPage()
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
}
================================================
FILE: cwidgets/compact/column.go
================================================
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)
}
================================================
FILE: cwidgets/compact/gauge.go
================================================
package compact
import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
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 {
*ui.Gauge
header string
fWidth int
}
func NewGaugeCol(header string) *GaugeCol {
g := &GaugeCol{ui.NewGauge(), header, 0}
g.Height = 1
g.Border = false
g.PaddingBottom = 0
g.Reset()
return g
}
func (w *GaugeCol) Reset() {
w.Label = "-"
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 {
if n <= 70 {
return ui.ThemeAttr("status.ok")
}
if n <= 90 {
return ui.ThemeAttr("status.warn")
}
return ui.ThemeAttr("status.danger")
}
================================================
FILE: cwidgets/compact/grid.go
================================================
package compact
import (
ui "github.com/gizak/termui"
)
type CompactGrid struct {
ui.GridBufferer
header *CompactHeader
cols []CompactCol // reference columns
Rows []RowBufferer
X, Y int
Width int
Height int
Offset int // starting row offset
}
func NewCompactGrid() *CompactGrid {
cg := &CompactGrid{header: NewCompactHeader()}
cg.rebuildHeader()
return cg
}
func (cg *CompactGrid) Align() {
y := cg.Y
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
cg.Offset = 0
}
// update row ypos, width recursively
colWidths := cg.calcWidths()
for _, r := range cg.pageRows() {
r.SetY(y)
y += r.GetHeight()
r.SetWidths(cg.Width, colWidths)
}
}
func (cg *CompactGrid) Clear() {
cg.Rows = []RowBufferer{}
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) SetY(y int) { cg.Y = y }
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - cg.header.Height - cg.Y }
// calculate and return per-column width
func (cg *CompactGrid) calcWidths() []int {
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:]...)
return rows
}
func (cg *CompactGrid) Buffer() ui.Buffer {
buf := ui.NewBuffer()
for _, r := range cg.pageRows() {
buf.Merge(r.Buffer())
}
return buf
}
func (cg *CompactGrid) AddRows(rows ...RowBufferer) {
cg.Rows = append(cg.Rows, rows...)
}
func (cg *CompactGrid) rebuildHeader() {
cg.cols = newRowWidgets()
cg.header.clearFieldPars()
for _, col := range cg.cols {
cg.header.addFieldPar(col.Header())
}
}
================================================
FILE: cwidgets/compact/header.go
================================================
package compact
import (
ui "github.com/gizak/termui"
)
type CompactHeader struct {
X, Y int
Width int
Height int
cols []CompactCol
widths []int
pars []*ui.Par
}
func NewCompactHeader() *CompactHeader {
return &CompactHeader{
X: rowPadding,
Height: 2,
}
}
func (row *CompactHeader) GetHeight() int {
return row.Height
}
func (row *CompactHeader) SetWidths(totalWidth int, widths []int) {
x := row.X
for n, w := range row.pars {
w.SetX(x)
w.SetWidth(widths[n])
x += widths[n] + colSpacing
}
row.Width = totalWidth
}
func (row *CompactHeader) SetX(x int) {
row.X = x
}
func (row *CompactHeader) SetY(y int) {
for _, p := range row.pars {
p.SetY(y)
}
row.Y = y
}
func (row *CompactHeader) Buffer() ui.Buffer {
buf := ui.NewBuffer()
for _, p := range row.pars {
buf.Merge(p.Buffer())
}
return buf
}
func (row *CompactHeader) clearFieldPars() {
row.pars = []*ui.Par{}
}
func (row *CompactHeader) addFieldPar(s string) {
p := ui.NewPar(s)
p.Height = row.Height
p.Border = false
row.pars = append(row.pars, p)
}
================================================
FILE: cwidgets/compact/row.go
================================================
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") }
================================================
FILE: cwidgets/compact/status.go
================================================
package compact
import (
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
// Status indicator
type Status struct {
*ui.Block
status []ui.Cell
health []ui.Cell
}
func NewStatus() CompactCol {
s := &Status{
Block: ui.NewBlock(),
status: []ui.Cell{{Ch: ' '}},
health: []ui.Cell{{Ch: ' '}},
}
s.Height = 1
s.Border = false
return s
}
func (s *Status) Buffer() ui.Buffer {
buf := s.Block.Buffer()
buf.Set(s.InnerX(), s.InnerY(), s.health[0])
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
var mark string
switch val {
case "":
return
case "created":
mark = "◉"
case "running":
mark = "▶"
color = ui.ThemeAttr("status.ok")
case "exited":
mark = "⏹"
color = ui.ThemeAttr("status.danger")
case "paused":
mark = "⏸"
default:
mark = " "
log.Warningf("unknown status string: \"%v\"", val)
}
s.status = ui.TextCells(mark, color, ui.ColorDefault)
}
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)
}
================================================
FILE: cwidgets/compact/text.go
================================================
package compact
import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
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 {
*ui.Par
header string
fWidth int
}
func NewTextCol(header string) *TextCol {
p := ui.NewPar("-")
p.Border = false
p.Height = 1
p.Width = 20
return &TextCol{
Par: p,
header: header,
fWidth: 0,
}
}
func (w *TextCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg")
w.TextFgColor = ui.ThemeAttr("par.text.hi")
w.TextBgColor = ui.ThemeAttr("par.text.fg")
}
func (w *TextCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg")
w.TextFgColor = ui.ThemeAttr("par.text.fg")
w.TextBgColor = ui.ThemeAttr("par.text.bg")
}
// TextCol implements CompactCol
func (w *TextCol) Reset() { w.setText("-") }
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) {
if w.fWidth > 0 && len(s) > w.fWidth {
s = s[0:w.fWidth]
}
w.Text = s
}
================================================
FILE: cwidgets/compact/util.go
================================================
package compact
// Common helper functions
import (
"fmt"
ui "github.com/gizak/termui"
)
const colSpacing = 1
func centerParText(p *ui.Par) {
var text string
var padding string
// strip existing left-padding
for i, ch := range p.Text {
if string(ch) != " " {
text = p.Text[i:]
break
}
}
padlen := (p.InnerWidth() - len(text)) / 2
for i := 0; i < padlen; i++ {
padding += " "
}
p.Text = fmt.Sprintf("%s%s", padding, text)
}
================================================
FILE: cwidgets/main.go
================================================
package cwidgets
import (
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
)
var log = logging.Init()
type WidgetUpdater interface {
SetMeta(models.Meta)
SetMetrics(models.Metrics)
}
type NullWidgetUpdater struct{}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMeta(models.Meta) {}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}
================================================
FILE: cwidgets/single/cpu.go
================================================
package single
import (
ui "github.com/gizak/termui"
)
type Cpu struct {
*ui.LineChart
hist FloatHist
}
func NewCpu() *Cpu {
cpu := &Cpu{ui.NewLineChart(), NewFloatHist(55)}
cpu.Mode = "dot"
cpu.BorderLabel = "CPU"
cpu.Height = 12
cpu.Width = colWidth[0]
cpu.X = 0
cpu.DataLabels = cpu.hist.Labels
// hack to force the default minY scale to 0
tmpData := []float64{20}
cpu.Data["CPU"] = tmpData
_ = cpu.Buffer()
cpu.Data["CPU"] = cpu.hist.Data
return cpu
}
func (w *Cpu) Update(val int) {
w.hist.Append(float64(val))
}
================================================
FILE: cwidgets/single/env.go
================================================
package single
import (
"regexp"
"strings"
ui "github.com/gizak/termui"
)
var envPattern = regexp.MustCompile(`(?P[^=]+)=(?P.*)`)
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)
if len(match) == 3 {
key := match[1]
value := match[2]
w.data[key] = value
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
}
}
w.Height = len(w.Rows) + 2
}
================================================
FILE: cwidgets/single/hist.go
================================================
package single
type IntHist struct {
Val int // most current data point
Data []int // historical data points
Labels []string
}
func NewIntHist(max int) *IntHist {
return &IntHist{
Data: make([]int, max),
Labels: make([]string, max),
}
}
func (h *IntHist) Append(val int) {
if len(h.Data) == cap(h.Data) {
h.Data = append(h.Data[:0], h.Data[1:]...)
}
h.Val = val
h.Data = append(h.Data, val)
}
type DiffHist struct {
*IntHist
lastVal int
}
func NewDiffHist(max int) *DiffHist {
return &DiffHist{NewIntHist(max), -1}
}
func (h *DiffHist) Append(val int) {
if h.lastVal >= 0 { // skip append if this is the initial update
diff := val - h.lastVal
h.IntHist.Append(diff)
}
h.lastVal = val
}
type FloatHist struct {
Val float64 // most current data point
Data []float64 // historical data points
Labels []string
}
func NewFloatHist(max int) FloatHist {
return FloatHist{
Data: make([]float64, max),
Labels: make([]string, max),
}
}
func (h FloatHist) Append(val float64) {
if len(h.Data) == cap(h.Data) {
h.Data = append(h.Data[:0], h.Data[1:]...)
}
h.Val = val
h.Data = append(h.Data, val)
}
================================================
FILE: cwidgets/single/info.go
================================================
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
}
================================================
FILE: cwidgets/single/io.go
================================================
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)
}
================================================
FILE: cwidgets/single/logs.go
================================================
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 }
================================================
FILE: cwidgets/single/main.go
================================================
package single
import (
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
var (
log = logging.Init()
sizeError = termSizeError()
colWidth = [2]int{65, 0} // left,right column width
)
type Single struct {
Info *Info
Net *Net
Cpu *Cpu
Mem *Mem
IO *IO
Env *Env
X, Y int
Width int
}
func NewSingle() *Single {
return &Single{
Info: NewInfo(),
Net: NewNet(),
Cpu: NewCpu(),
Mem: NewMem(),
IO: NewIO(),
Env: NewEnv(),
Width: ui.TermWidth(),
}
}
func (e *Single) Up() {
if e.Y < 0 {
e.Y++
e.Align()
ui.Render(e)
}
}
func (e *Single) Down() {
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)
}
}
}
func (e *Single) SetMetrics(m models.Metrics) {
e.Cpu.Update(m.CPUUtil)
e.Net.Update(m.NetRx, m.NetTx)
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
}
// GetHeight returns total column height
func (e *Single) GetHeight() (h int) {
h += e.Info.Height
h += e.Net.Height
h += e.Cpu.Height
h += e.Mem.Height
h += e.IO.Height
h += e.Env.Height
return h
}
func (e *Single) Align() {
// reset offset if needed
if e.GetHeight() <= ui.TermHeight() {
e.Y = 0
}
y := e.Y
for _, i := range e.all() {
i.SetY(y)
y += i.GetHeight()
}
if e.Width > colWidth[0] {
colWidth[1] = e.Width - (colWidth[0] + 1)
}
e.Mem.Align()
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
}
func (e *Single) Buffer() ui.Buffer {
buf := ui.NewBuffer()
if e.Width < (colWidth[0] + colWidth[1]) {
ui.Clear()
buf.Merge(sizeError.Buffer())
return buf
}
buf.Merge(e.Info.Buffer())
buf.Merge(e.Cpu.Buffer())
buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
buf.Merge(e.Env.Buffer())
return buf
}
func (e *Single) all() []ui.GridBufferer {
return []ui.GridBufferer{
e.Info,
e.Cpu,
e.Mem,
e.Net,
e.IO,
e.Env,
}
}
func termSizeError() *ui.Par {
p := ui.NewPar("screen too small!")
p.Height = 1
p.Width = 20
p.Border = false
return p
}
================================================
FILE: cwidgets/single/mem.go
================================================
package single
import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
ui "github.com/gizak/termui"
)
type Mem struct {
*ui.Block
Chart *ui.MBarChart
InnerLabel *ui.Par
valHist *IntHist
limitHist *IntHist
}
func NewMem() *Mem {
mem := &Mem{
Block: ui.NewBlock(),
Chart: newMemChart(),
InnerLabel: newMemLabel(),
valHist: NewIntHist(9),
limitHist: NewIntHist(9),
}
mem.Height = 13
mem.Width = colWidth[0]
mem.BorderLabel = "MEM"
mem.Chart.Data[0] = mem.valHist.Data
mem.Chart.Data[1] = mem.limitHist.Data
mem.Chart.DataLabels = mem.valHist.Labels
return mem
}
func (w *Mem) Align() {
y := w.Y + 1
w.InnerLabel.SetY(y)
w.Chart.SetY(y + w.InnerLabel.Height)
w.Chart.Height = w.Height - w.InnerLabel.Height - 2
w.Chart.SetWidth(w.Width - 2)
}
func (w *Mem) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(w.Block.Buffer())
buf.Merge(w.InnerLabel.Buffer())
buf.Merge(w.Chart.Buffer())
return buf
}
func newMemLabel() *ui.Par {
p := ui.NewPar("-")
p.X = 1
p.Border = false
p.Height = 1
p.Width = 20
return p
}
func newMemChart() *ui.MBarChart {
mbar := ui.NewMBarChart()
mbar.X = 1
mbar.Border = false
mbar.BarGap = 1
mbar.BarWidth = 6
mbar.BarColor[1] = ui.ColorBlack
mbar.NumColor[1] = ui.ColorBlack
mbar.NumFmt = cwidgets.ByteFormatShort
//mbar.ShowScale = true
return mbar
}
func (w *Mem) Update(val int, limit int) {
w.valHist.Append(val)
w.limitHist.Append(limit - val)
w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatShort(val), cwidgets.ByteFormatShort(limit))
//w.Data[0] = w.hist.data
}
================================================
FILE: cwidgets/single/net.go
================================================
package single
import (
"fmt"
"strings"
"github.com/bcicen/ctop/cwidgets"
ui "github.com/gizak/termui"
)
type Net struct {
*ui.Sparklines
rxHist *DiffHist
txHist *DiffHist
}
func NewNet() *Net {
net := &Net{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}
net.BorderLabel = "NET"
net.Height = 6
net.Width = colWidth[0]
net.X = 0
net.Y = 24
rx := ui.NewSparkline()
rx.Title = "RX"
rx.Height = 1
rx.Data = net.rxHist.Data
rx.LineColor = ui.ColorGreen
tx := ui.NewSparkline()
tx.Title = "TX"
tx.Height = 1
tx.Data = net.txHist.Data
tx.LineColor = ui.ColorYellow
net.Lines = []ui.Sparkline{rx, tx}
return net
}
func (w *Net) Update(rx int64, tx int64) {
var rate string
w.rxHist.Append(int(rx))
rate = strings.ToLower(cwidgets.ByteFormat(w.rxHist.Val))
w.Lines[0].Title = fmt.Sprintf("RX [%s/s]", rate)
w.txHist.Append(int(tx))
rate = strings.ToLower(cwidgets.ByteFormat(w.txHist.Val))
w.Lines[1].Title = fmt.Sprintf("TX [%s/s]", rate)
}
================================================
FILE: cwidgets/util.go
================================================
package cwidgets
import (
"strconv"
)
const (
// byte ratio constants
_ = iota
kib float64 = 1 << (10 * iota)
mib
gib
tib
pib
)
var (
units = []float64{
1,
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 {
return strconv.FormatFloat(f, 'f', getPrecision(f, maxp), 64)
}
func getPrecision(f float64, maxp int) int {
frac := int((f - float64(int(f))) * 100)
if frac == 0 || maxp == 0 {
return 0
}
if frac%10 == 0 || maxp < 2 {
return 1
}
return maxp
}
================================================
FILE: debug.go
================================================
package main
import (
"fmt"
"reflect"
"runtime"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui"
)
var mstats = &runtime.MemStats{}
func logEvent(e ui.Event) {
// skip timer events e.g. /timer/1s
if e.From == "timer" {
return
}
var s string
s += fmt.Sprintf("Type=%s", quote(e.Type))
s += fmt.Sprintf(" Path=%s", quote(e.Path))
s += fmt.Sprintf(" From=%s", quote(e.From))
if e.To != "" {
s += fmt.Sprintf(" To=%s", quote(e.To))
}
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
func dumpContainer(c *container.Container) {
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
for k, v := range c.Meta {
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
}
msg += inspect(&c.Metrics)
log.Infof(msg)
}
func inspect(i interface{}) (s string) {
val := reflect.ValueOf(i)
elem := val.Type().Elem()
eName := elem.String()
for i := 0; i < elem.NumField(); i++ {
field := elem.Field(i)
fieldVal := reflect.Indirect(val).FieldByName(field.Name)
s += fmt.Sprintf("%s.%s = ", eName, field.Name)
s += fmt.Sprintf("%v (%s)\n", fieldVal, field.Type)
}
return s
}
func quote(s string) string {
return fmt.Sprintf("\"%s\"", s)
}
================================================
FILE: go.mod
================================================
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.1-0.20180817033724-8d4faad06196+incompatible
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
github.com/mattn/go-runewidth v0.0.2
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
github.com/opencontainers/runc v1.1.0
github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.4.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/Microsoft/hcsshim v0.8.10 // indirect
github.com/checkpoint-restore/go-criu/v5 v5.3.0 // indirect
github.com/cilium/ebpf v0.7.0 // indirect
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.4.1 // indirect
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/maruel/panicparse v1.6.1 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/moby/sys/mount v0.2.0 // indirect
github.com/moby/sys/mountinfo v0.5.0 // indirect
github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mrunalp/fileutils v0.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect
github.com/opencontainers/selinux v1.10.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
go.opencensus.io v0.22.0 // indirect
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
go 1.18
================================================
FILE: go.sum
================================================
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/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/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
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.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
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.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/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.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
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/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/gizak/termui v2.3.1-0.20180817033724-8d4faad06196+incompatible h1:pUbrySwhNIu18YXjMTCt/Z3kr8eYQ8hRDs4BeR/crmA=
github.com/gizak/termui v2.3.1-0.20180817033724-8d4faad06196+incompatible/go.mod h1:PkJoWUt/zacQKysNfQtcw1RW+eK2SxkieVBtl+4ovLA=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/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.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
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/maruel/panicparse v1.6.1 h1:803MjBzGcUgE1vYgg3UMNq3G1oyYeKkMu3t6hBS97x0=
github.com/maruel/panicparse v1.6.1/go.mod h1:uoxI4w9gJL6XahaYPMq/z9uadrdr1SyHuQwV2q80Mm0=
github.com/maruel/panicparse/v2 v2.1.1/go.mod h1:AeTWdCE4lcq8OKsLb6cHSj1RWHVSnV9HBCk7sKLF4Jg=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
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.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI=
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
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-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/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.1.0 h1:O9+X96OcDjkmmZyfaG996kV7yq8HsoU2h1XRRQcefG8=
github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=
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.10.0 h1:rAiKF8hTcgLI3w0DHm6i0ylVVcOrlgR1kK99DRLDhyU=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
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/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/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.2-0.20210429002308-3879420cc921 h1:58EBmR2dMNL2n/FnbQewK3D14nXr0V9CObDSvMJLq+Y=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
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.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/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=
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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/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-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c h1:DHcbWVXeY+0Y8HHKR+rbLwnoh2F4tNCY7rTiHJ30RmA=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/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.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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=
================================================
FILE: grid.go
================================================
package main
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/cwidgets/single"
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) {
// reinit body rows
cGrid.Clear()
// build layout
y := 1
if config.GetSwitchVal("enableHeader") {
header.SetCount(cursor.Len())
header.SetFilter(config.GetVal("filterStr"))
y += header.Height()
}
cGrid.SetY(y)
for _, c := range cursor.filtered {
cGrid.AddRows(c.Widgets)
}
if clr {
ui.Clear()
log.Debugf("screen cleared")
}
if config.GetSwitchVal("enableHeader") {
ui.Render(header)
}
cGrid.Align()
ui.Render(cGrid)
}
func SingleView() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
ex := single.NewSingle()
c.SetUpdater(ex)
ex.Align()
ui.Render(ex)
HandleKeys("up", ex.Up)
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) {
ex.SetWidth(ui.TermWidth())
ex.Align()
log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows())
})
ui.Loop()
c.SetUpdater(c.Widgets)
return nil
}
func RefreshDisplay() error {
// skip display refresh during scroll
if !cursor.isScrolling {
needsClear, err := cursor.RefreshContainers()
if err != nil {
return err
}
RedrawRows(needsClear)
}
return nil
}
func Display() bool {
var menu MenuFn
var connErr error
cGrid.SetWidth(ui.TermWidth())
ui.DefaultEvtStream.Hook(logEvent)
// initial draw
header.Align()
status.Align()
cursor.RefreshContainers()
RedrawRows(true)
HandleKeys("up", cursor.Up)
HandleKeys("down", 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/", func(ui.Event) {
menu = ContainerMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/", 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.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers")
connErr = RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
})
ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected())
})
ui.Handle("/sys/kbd/f", func(ui.Event) {
menu = FilterMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/H", func(ui.Event) {
config.Toggle("enableHeader")
RedrawRows(true)
})
ui.Handle("/sys/kbd/r", func(e ui.Event) {
config.Toggle("sortReversed")
})
ui.Handle("/sys/kbd/s", func(ui.Event) {
menu = SortMenu
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) {
if log.StatusQueued() {
ui.StopLoop()
}
connErr = RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
header.Align()
status.Align()
cursor.ScrollPage()
cGrid.SetWidth(ui.TermWidth())
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
RedrawRows(true)
})
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 {
for menu != nil {
menu = menu()
}
return false
}
return true
}
================================================
FILE: install.sh
================================================
#!/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!"
================================================
FILE: keys.go
================================================
package main
import (
ui "github.com/gizak/termui"
)
// Common action keybindings
var keyMap = map[string][]string{
"up": []string{
"/sys/kbd/",
"/sys/kbd/k",
},
"down": []string{
"/sys/kbd/",
"/sys/kbd/j",
},
"pgup": []string{
"/sys/kbd/",
"/sys/kbd/C-",
},
"pgdown": []string{
"/sys/kbd/",
"/sys/kbd/C-",
},
"exit": []string{
"/sys/kbd/q",
"/sys/kbd/C-c",
"/sys/kbd/",
},
"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() })
}
}
================================================
FILE: logging/main.go
================================================
package logging
import (
"fmt"
"os"
"time"
"github.com/op/go-logging"
)
const (
size = 1024
)
var (
Log *CTopLogger
exited bool
level = logging.INFO // default level
format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
)
)
type statusMsg struct {
Text string
IsError bool
}
type CTopLogger struct {
*logging.Logger
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 {
if Log == nil {
logging.SetFormatter(format) // setup default formatter
Log = &CTopLogger{
logging.MustGetLogger("ctop"),
logging.NewMemoryBackend(size),
nil,
[]statusMsg{},
}
debugMode := debugMode()
if debugMode {
level = logging.DEBUG
}
backendLvl := logging.AddModuleLevel(Log.backend)
backendLvl.SetLevel(level, "")
logFilePath := debugModeFile()
if logFilePath == "" {
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")
}
return Log
}
func (log *CTopLogger) tail() chan string {
stream := make(chan string)
node := log.backend.Head()
go func() {
for {
stream <- node.Record.Formatted(0)
for {
nnode := node.Next()
if nnode != nil {
node = nnode
break
}
if exited {
close(stream)
return
}
time.Sleep(1 * time.Second)
}
}
}()
return stream
}
func (log *CTopLogger) Exit() {
exited = true
if log.logFile != nil {
_ = log.logFile.Close()
}
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") }
================================================
FILE: logging/server.go
================================================
package logging
import (
"fmt"
"io"
"net"
"sync"
)
const (
socketPath = "./ctop.sock"
socketAddr = "0.0.0.0:9000"
)
var server struct {
wg sync.WaitGroup
ln net.Listener
}
func getListener() net.Listener {
var ln net.Listener
var err error
if debugModeTCP() {
ln, err = net.Listen("tcp", socketAddr)
} else {
ln, err = net.Listen("unix", socketPath)
}
if err != nil {
panic(err)
}
return ln
}
func StartServer() {
server.ln = getListener()
go func() {
for {
conn, err := server.ln.Accept()
if err != nil {
if err, ok := err.(net.Error); ok && err.Temporary() {
continue
}
return
}
go handler(conn)
}
}()
Log.Notice("logging server started")
}
func StopServer() {
server.wg.Wait()
if server.ln != nil {
server.ln.Close()
}
}
func handler(wc io.WriteCloser) {
server.wg.Add(1)
defer server.wg.Done()
defer wc.Close()
for msg := range Log.tail() {
msg = fmt.Sprintf("%s\n", msg)
wc.Write([]byte(msg))
}
wc.Write([]byte("bye\n"))
}
================================================
FILE: main.go
================================================
package main
import (
"flag"
"fmt"
"os"
"runtime"
"strings"
"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/logging"
"github.com/bcicen/ctop/widgets"
ui "github.com/gizak/termui"
tm "github.com/nsf/termbox-go"
)
var (
build = "none"
version = "dev-build"
goVersion = runtime.Version()
log *logging.CTopLogger
cursor *GridCursor
cGrid *compact.CompactGrid
header *widgets.CTopHeader
status *widgets.StatusLine
errView *widgets.ErrorView
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
)
func main() {
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
if *invertFlag {
InvertColorMap()
}
ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil {
panic(err)
}
tm.SetInputMode(tm.InputAlt)
defer Shutdown()
// init grid, cursor, header
cSuper, err := connector.ByName(*connectorFlag)
if err != nil {
panic(err)
}
cursor = &GridCursor{cSuper: cSuper}
cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader()
status = widgets.NewStatusLine()
errView = widgets.NewErrorView()
for {
exit := Display()
if exit {
return
}
}
}
func Shutdown() {
log.Notice("shutting down")
log.Exit()
if tm.IsInit {
ui.Close()
}
}
// ensure a given sort field is valid
func validSort(s string) {
if _, ok := container.Sorters[s]; !ok {
fmt.Printf("invalid sort field: %s\n", s)
os.Exit(1)
}
}
func panicExit() {
if r := recover(); r != nil {
Shutdown()
panic(r)
fmt.Printf("error: %s\n", r)
os.Exit(1)
}
}
var helpMsg = `ctop - interactive container viewer
usage: ctop [options]
options:
`
func printHelp() {
fmt.Println(helpMsg)
flag.PrintDefaults()
fmt.Printf("\navailable connectors: ")
fmt.Println(strings.Join(connector.Enabled(), ", "))
}
================================================
FILE: menus.go
================================================
package main
import (
"fmt"
"strings"
"time"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/widgets"
"github.com/bcicen/ctop/widgets/menu"
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{
{" - open container menu", ""},
{"", ""},
{"[a] - toggle display of all containers", ""},
{"[f] - filter displayed containers", ""},
{"[h] - open this help dialog", ""},
{"[H] - toggle ctop header", ""},
{"[s] - select container sort field", ""},
{"[r] - reverse container sort order", ""},
{"[o] - open single view", ""},
{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
{"[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 {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.BorderLabel = "Help"
m.AddItems(helpDialog...)
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Clear()
ui.Render(m)
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
return nil
}
func FilterMenu() MenuFn {
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
i := widgets.NewInput()
i.BorderLabel = "Filter"
i.SetY(ui.TermHeight() - i.Height)
i.Data = config.GetVal("filterStr")
ui.Render(i)
// refresh container rows on input
stream := i.Stream()
go func() {
for s := range stream {
config.Update("filterStr", s)
RefreshDisplay()
ui.Render(i)
}
}()
i.InputHandlers()
ui.Handle("/sys/kbd/", func(ui.Event) {
config.Update("filterStr", "")
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
config.Update("filterStr", i.Data)
ui.StopLoop()
})
ui.Loop()
return nil
}
func SortMenu() MenuFn {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.SortItems = true
m.BorderLabel = "Sort Field"
for _, field := range container.SortFields() {
m.AddItems(menu.Item{field, ""})
}
// set cursor position to current sort field
m.SetCursor(config.GetVal("sortField"))
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/", func(ui.Event) {
config.Update("sortField", m.SelectedValue())
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: / "
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/", 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/", func(ui.Event) {
selected = m.SelectedValue()
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
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/", 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) }
================================================
FILE: models/main.go
================================================
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,
}
}
================================================
FILE: widgets/error.go
================================================
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))
}
================================================
FILE: widgets/header.go
================================================
package widgets
import (
"fmt"
"time"
ui "github.com/gizak/termui"
)
type CTopHeader struct {
Time *ui.Par
Count *ui.Par
Filter *ui.Par
bg *ui.Par
}
func NewCTopHeader() *CTopHeader {
return &CTopHeader{
Time: headerPar(2, ""),
Count: headerPar(24, "-"),
Filter: headerPar(40, ""),
bg: headerBg(),
}
}
func (c *CTopHeader) Buffer() ui.Buffer {
buf := ui.NewBuffer()
c.Time.Text = timeStr()
buf.Merge(c.bg.Buffer())
buf.Merge(c.Time.Buffer())
buf.Merge(c.Count.Buffer())
buf.Merge(c.Filter.Buffer())
return buf
}
func (c *CTopHeader) Align() {
c.bg.SetWidth(ui.TermWidth() - 1)
}
func (c *CTopHeader) Height() int {
return c.bg.Height
}
func headerBgBordered() *ui.Par {
bg := ui.NewPar("")
bg.X = 1
bg.Height = 3
bg.Bg = ui.ThemeAttr("header.bg")
return bg
}
func headerBg() *ui.Par {
bg := ui.NewPar("")
bg.X = 1
bg.Height = 1
bg.Border = false
bg.Bg = ui.ThemeAttr("header.bg")
return bg
}
func (c *CTopHeader) SetCount(val int) {
c.Count.Text = fmt.Sprintf("%d containers", val)
}
func (c *CTopHeader) SetFilter(val string) {
if val == "" {
c.Filter.Text = ""
} else {
c.Filter.Text = fmt.Sprintf("filter: %s", val)
}
}
func timeStr() string {
ts := time.Now().Local().Format("15:04:05 MST")
return fmt.Sprintf("ctop - %s", ts)
}
func headerPar(x int, s string) *ui.Par {
p := ui.NewPar(fmt.Sprintf(" %s", s))
p.X = x
p.Border = false
p.Height = 1
p.Width = 20
p.Bg = ui.ThemeAttr("header.bg")
p.TextFgColor = ui.ThemeAttr("header.fg")
p.TextBgColor = ui.ThemeAttr("header.bg")
return p
}
================================================
FILE: widgets/input.go
================================================
package widgets
import (
"strings"
ui "github.com/gizak/termui"
)
var (
input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
)
type Padding [2]int // x,y padding
type Input struct {
ui.Block
Label string
Data string
MaxLen int
TextFgColor ui.Attribute
TextBgColor ui.Attribute
stream chan string // stream text as it changes
padding Padding
}
func NewInput() *Input {
i := &Input{
Block: *ui.NewBlock(),
Label: "input",
MaxLen: 20,
TextFgColor: ui.ThemeAttr("menu.text.fg"),
TextBgColor: ui.ThemeAttr("menu.text.bg"),
padding: Padding{4, 2},
}
i.BorderFg = ui.ThemeAttr("menu.border.fg")
i.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
i.calcSize()
return i
}
func (i *Input) calcSize() {
i.Height = 3 // minimum height
i.Width = i.MaxLen + (i.padding[0] * 2)
}
func (i *Input) Buffer() ui.Buffer {
var cell ui.Cell
buf := i.Block.Buffer()
x := i.Block.X + i.padding[0]
y := i.Block.Y + 1
for _, ch := range i.Data {
cell = ui.Cell{Ch: ch, Fg: i.TextFgColor, Bg: i.TextBgColor}
buf.Set(x, y, cell)
x++
}
return buf
}
func (i *Input) Stream() chan string {
i.stream = make(chan string)
return i.stream
}
func (i *Input) KeyPress(e ui.Event) {
ch := strings.Replace(e.Path, "/sys/kbd/", "", -1)
if ch == "C-8" {
idx := len(i.Data) - 1
if idx > -1 {
i.Data = i.Data[0:idx]
i.stream <- i.Data
}
ui.Render(i)
return
}
if len(i.Data) >= i.MaxLen {
return
}
if strings.Contains(input_chars, ch) {
i.Data += ch
i.stream <- i.Data
ui.Render(i)
}
}
// Setup some default handlers for menu navigation
func (i *Input) InputHandlers() {
ui.Handle("/sys/kbd/", i.KeyPress)
}
================================================
FILE: widgets/menu/items.go
================================================
package menu
type Item struct {
Val string
Label string
}
// Use label as display text of item, if given
func (m Item) Text() string {
if m.Label != "" {
return m.Label
}
return m.Val
}
type Items []Item
func NewItems(items ...Item) (mitems Items) {
for _, i := range items {
mitems = append(mitems, i)
}
return mitems
}
// Sort methods for Items
func (m Items) Len() int { return len(m) }
func (m Items) Swap(a, b int) { m[a], m[b] = m[b], m[a] }
func (m Items) Less(a, b int) bool {
return m[a].Text() < m[b].Text()
}
================================================
FILE: widgets/menu/main.go
================================================
package menu
import (
"sort"
ui "github.com/gizak/termui"
)
type Padding [2]int // x,y padding
type Menu struct {
ui.Block
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
TextBgColor ui.Attribute
cursorPos int
items Items
padding Padding
toolTip *ToolTip
}
func NewMenu() *Menu {
m := &Menu{
Block: *ui.NewBlock(),
TextFgColor: ui.ThemeAttr("menu.text.fg"),
TextBgColor: ui.ThemeAttr("menu.text.bg"),
cursorPos: 0,
padding: Padding{4, 2},
}
m.BorderFg = ui.ThemeAttr("menu.border.fg")
m.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
m.X = 1
return m
}
// Append Item to Menu
func (m *Menu) AddItems(items ...Item) {
for _, i := range items {
m.items = append(m.items, i)
}
m.refresh()
}
// DelItem removes menu item by value or label
func (m *Menu) DelItem(s string) (success bool) {
for n, i := range m.items {
if i.Val == s || i.Label == s {
m.items = append(m.items[:n], m.items[n+1:]...)
success = true
m.refresh()
break
}
}
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
func (m *Menu) SetCursor(s string) (success bool) {
for n, i := range m.items {
if i.Val == s || i.Label == s {
m.cursorPos = n
return true
}
}
return false
}
// SetToolTip sets an optional tooltip string to show at bottom of screen
func (m *Menu) SetToolTip(lines ...string) {
m.toolTip = NewToolTip(lines...)
}
func (m *Menu) SelectedItem() Item {
return m.items[m.cursorPos]
}
func (m *Menu) SelectedValue() string {
return m.items[m.cursorPos].Val
}
func (m *Menu) Buffer() ui.Buffer {
var cell ui.Cell
buf := m.Block.Buffer()
y := m.Y + m.padding[1]
if m.SubText != "" {
x := m.X + m.padding[0]
for i, ch := range m.SubText {
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
buf.Set(x+i, y, cell)
}
y += 2
}
for n, item := range m.items {
x := m.X + m.padding[0]
for _, ch := range item.Text() {
// invert bg/fg colors on currently selected row
if m.Selectable && n == m.cursorPos {
cell = ui.Cell{Ch: ch, Fg: ui.ColorBlack, Bg: m.TextFgColor}
} else {
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
}
buf.Set(x, y+n, cell)
x++
}
}
if m.toolTip != nil {
buf.Merge(m.toolTip.Buffer())
}
return buf
}
func (m *Menu) Up() {
if m.cursorPos > 0 {
m.cursorPos--
ui.Render(m)
}
}
func (m *Menu) Down() {
if m.cursorPos < (len(m.items) - 1) {
m.cursorPos++
ui.Render(m)
}
}
// Sort menu items(if enabled) and re-calculate window size
func (m *Menu) refresh() {
if m.SortItems {
sort.Sort(m.items)
}
m.calcSize()
ui.Render(m)
}
// Set width and height based on menu items
func (m *Menu) calcSize() {
m.Width = 7 // minimum width
var height int
for _, i := range m.items {
s := i.Text()
if len(s) > m.Width {
m.Width = len(s)
}
height++
}
if m.SubText != "" {
if len(m.SubText) > m.Width {
m.Width = len(m.SubText)
}
height += 2
}
m.Width += (m.padding[0] * 2)
m.Height = height + (m.padding[1] * 2)
}
================================================
FILE: widgets/menu/tooltip.go
================================================
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()
}
================================================
FILE: widgets/status.go
================================================
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
}
================================================
FILE: widgets/view.go
================================================
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:]
}
}
================================================
FILE: widgets/view_test.go
================================================
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))
}
}