Full Code of bcicen/ctop for AI

master 59f00dd6aaeb cached
75 files
161.3 KB
57.1k tokens
515 symbols
1 requests
Download .txt
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
================================================
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>

#

![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:
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>

as well as a [single container view][single_view] for inspecting a specific container.

`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 <string>` | set an initial filter string
`-h`	| display help dialog
`-i`  | invert default colors
`-r`	| reverse container sort order
`-s`  | select initial container sort field
`-v`	| output version information and exit

### Keybindings

|           Key            | Action                                                     |
| :----------------------: | ---------------------------------------------------------- |
| <kbd>&lt;ENTER&gt;</kbd> | Open container menu                                        |
|       <kbd>a</kbd>       | Toggle display of all (running and non-running) containers |
|       <kbd>f</kbd>       | Filter displayed containers (`esc` to clear when open)     |
|       <kbd>H</kbd>       | Toggle ctop header                                         |
|       <kbd>h</kbd>       | Open help dialog                                           |
|       <kbd>s</kbd>       | Select container sort field                                |
|       <kbd>r</kbd>       | Reverse container sort order                               |
|       <kbd>o</kbd>       | Open single view                                           |
|       <kbd>l</kbd>       | View container logs (`t` to toggle timestamp when open)    |
|       <kbd>e</kbd>       | Exec Shell                                                 |
|       <kbd>c</kbd>       | Configure columns                                          |
|       <kbd>S</kbd>       | Save current configuration to file                         |
|       <kbd>q</kbd>       | Quit ctop                                                  |

[build]: _docs/build.md
[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:

![Debug in GoLand](img/goland_debug.png)


================================================
FILE: _docs/single.md
================================================
# Single Container View

ctop provides a rolling, single container view for following metrics
<p align="center"><img width="80%" src="img/single.gif" alt="ctop"/></p>


================================================
FILE: _docs/status.md
================================================
# Status Indicator

The `ctop` grid view provides a compact status indicator to convey container state

<img width="200px" src="img/status.png" alt="ctop"/>

### Status

<span align="center">

Appearance | Description
--- | ---
red | container is stopped
green | container is running
▮▮ | container is paused

</span>

### Health
If the container is configured with a health check, a `+` will appear next to the indicator

<span align="center">

Appearance | Description
--- | ---
red | health check in failed state
yellow | health check in starting state
green | health check in OK state

</span>


================================================
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<KEY>[^=]+)=(?P<VALUJE>.*)`)

type Env struct {
	*ui.Table
	data map[string]string
}

func NewEnv() *Env {
	p := ui.NewTable()
	p.Height = 4
	p.Width = colWidth[0]
	p.FgColor = ui.ThemeAttr("par.text.fg")
	p.Separator = false
	i := &Env{p, make(map[string]string)}
	i.BorderLabel = "Env"
	return i
}

func (w *Env) Set(allEnvs string) {
	envs := strings.Split(allEnvs, ";")
	w.Rows = [][]string{}
	for _, env := range envs {
		match := envPattern.FindStringSubmatch(env)
		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/<enter>", func(ui.Event) {
		menu = ContainerMenu
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/<left>", func(ui.Event) {
		menu = LogMenu
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/<right>", func(ui.Event) {
		menu = SingleView
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/l", func(ui.Event) {
		menu = LogMenu
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/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/<up>",
		"/sys/kbd/k",
	},
	"down": []string{
		"/sys/kbd/<down>",
		"/sys/kbd/j",
	},
	"pgup": []string{
		"/sys/kbd/<previous>",
		"/sys/kbd/C-<up>",
	},
	"pgdown": []string{
		"/sys/kbd/<next>",
		"/sys/kbd/C-<down>",
	},
	"exit": []string{
		"/sys/kbd/q",
		"/sys/kbd/C-c",
		"/sys/kbd/<escape>",
	},
	"help": []string{
		"/sys/kbd/h",
		"/sys/kbd/?",
	},
}

// Apply a common handler function to all given keys
func HandleKeys(i string, f func()) {
	for _, k := range keyMap[i] {
		ui.Handle(k, func(ui.Event) { f() })
	}
}


================================================
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{
	{"<enter> - open container menu", ""},
	{"", ""},
	{"[a] - toggle display of all containers", ""},
	{"[f] - filter displayed containers", ""},
	{"[h] - open this help dialog", ""},
	{"[H] - toggle ctop header", ""},
	{"[s] - select container sort field", ""},
	{"[r] - reverse container sort order", ""},
	{"[o] - open single view", ""},
	{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
	{"[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/<escape>", func(ui.Event) {
		config.Update("filterStr", "")
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/<enter>", 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/<enter>", 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: <Page Up> / <Page Down>"

	rebuild := func() {
		// get padding for right alignment of enabled status
		var maxLen int
		for _, col := range config.GlobalColumns {
			if len(col.Label) > maxLen {
				maxLen = len(col.Label)
			}
		}
		maxLen += padding

		// rebuild menu items
		m.ClearItems()
		for _, col := range config.GlobalColumns {
			txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label))
			if col.Enabled {
				txt += enabledStr
			} else {
				txt += disabledStr
			}
			m.AddItems(menu.Item{col.Name, txt})
		}
	}

	upFn := func() {
		config.ColumnLeft(m.SelectedValue())
		m.Up()
		rebuild()
	}

	downFn := func() {
		config.ColumnRight(m.SelectedValue())
		m.Down()
		rebuild()
	}

	toggleFn := func() {
		config.ColumnToggle(m.SelectedValue())
		rebuild()
	}

	rebuild()

	HandleKeys("up", m.Up)
	HandleKeys("down", m.Down)
	HandleKeys("enter", toggleFn)
	HandleKeys("pgup", upFn)
	HandleKeys("pgdown", downFn)

	ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() })
	ui.Handle("/sys/kbd/<enter>", func(ui.Event) { toggleFn() })

	HandleKeys("exit", func() {
		cSource, err := cursor.cSuper.Get()
		if err == nil {
			for _, c := range cSource.All() {
				c.RecreateWidgets()
			}
		}
		ui.StopLoop()
	})

	ui.Render(m)
	ui.Loop()
	return nil
}

func ContainerMenu() MenuFn {
	c := cursor.Selected()
	if c == nil {
		return nil
	}

	ui.DefaultEvtStream.ResetHandlers()
	defer ui.DefaultEvtStream.ResetHandlers()

	m := menu.NewMenu()
	m.Selectable = true
	m.BorderLabel = "Menu"

	items := []menu.Item{
		menu.Item{Val: "single", Label: "[o] single view"},
		menu.Item{Val: "logs", Label: "[l] log view"},
	}

	if c.Meta["state"] == "running" {
		items = append(items, menu.Item{Val: "stop", Label: "[s] stop"})
		items = append(items, menu.Item{Val: "pause", Label: "[p] pause"})
		items = append(items, menu.Item{Val: "restart", Label: "[r] restart"})
		items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"})
		if c.Meta["Web Port"] != "" {
			items = append(items, menu.Item{Val: "browser", Label: "[w] open in browser"})
		}
	}
	if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
		items = append(items, menu.Item{Val: "start", Label: "[s] start"})
		items = append(items, menu.Item{Val: "remove", Label: "[R] remove"})
	}
	if c.Meta["state"] == "paused" {
		items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"})
	}
	items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"})

	m.AddItems(items...)
	ui.Render(m)

	HandleKeys("up", m.Up)
	HandleKeys("down", m.Down)

	var selected string

	// shortcuts
	ui.Handle("/sys/kbd/o", func(ui.Event) {
		selected = "single"
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/l", func(ui.Event) {
		selected = "logs"
		ui.StopLoop()
	})
	if c.Meta["state"] != "paused" {
		ui.Handle("/sys/kbd/s", func(ui.Event) {
			if c.Meta["state"] == "running" {
				selected = "stop"
			} else {
				selected = "start"
			}
			ui.StopLoop()
		})
	}
	if c.Meta["state"] != "exited" && c.Meta["state"] != "created" {
		ui.Handle("/sys/kbd/p", func(ui.Event) {
			if c.Meta["state"] == "paused" {
				selected = "unpause"
			} else {
				selected = "pause"
			}
			ui.StopLoop()
		})
	}
	if c.Meta["state"] == "running" {
		ui.Handle("/sys/kbd/e", func(ui.Event) {
			selected = "exec"
			ui.StopLoop()
		})
		ui.Handle("/sys/kbd/r", func(ui.Event) {
			selected = "restart"
			ui.StopLoop()
		})
		if c.Meta["Web Port"] != "" {
			ui.Handle("/sys/kbd/w", func(ui.Event) {
				selected = "browser"
			})
		}
	}
	ui.Handle("/sys/kbd/R", func(ui.Event) {
		selected = "remove"
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/c", func(ui.Event) {
		ui.StopLoop()
	})

	ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
		selected = m.SelectedValue()
		ui.StopLoop()
	})
	ui.Handle("/sys/kbd/", func(ui.Event) {
		ui.StopLoop()
	})
	ui.Loop()

	var nextMenu MenuFn
	switch selected {
	case "single":
		nextMenu = SingleView
	case "logs":
		nextMenu = LogMenu
	case "exec":
		nextMenu = ExecShell
	case "browser":
		nextMenu = OpenInBrowser
	case "start":
		nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
	case "stop":
		nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
	case "remove":
		nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
	case "pause":
		nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
	case "unpause":
		nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
	case "restart":
		nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
	}

	return nextMenu
}

func LogMenu() MenuFn {

	c := cursor.Selected()
	if c == nil {
		return nil
	}

	ui.DefaultEvtStream.ResetHandlers()
	defer ui.DefaultEvtStream.ResetHandlers()

	logs, quit := logReader(c)
	m := widgets.NewTextView(logs)
	m.BorderLabel = fmt.Sprintf("Logs [%s]", c.GetMeta("name"))
	ui.Render(m)

	ui.Handle("/sys/wnd/resize", func(e ui.Event) {
		m.Resize()
	})
	ui.Handle("/sys/kbd/t", func(ui.Event) {
		m.Toggle()
	})
	ui.Handle("/sys/kbd/", func(ui.Event) {
		quit <- true
		ui.StopLoop()
	})
	ui.Loop()
	return nil
}

func ExecShell() MenuFn {
	c := cursor.Selected()

	if c == nil {
		return nil
	}

	ui.DefaultEvtStream.ResetHandlers()
	defer ui.DefaultEvtStream.ResetHandlers()
	// Detect and execute default shell in container.
	// Execute Ash shell command: /bin/sh -c
	// Reset colors: printf '\e[0m\e[?25h'
	// Clear screen
	// Run default shell for the user. It's configured in /etc/passwd and looks like root:x:0:0:root:/root:/bin/bash:
	//  1. Get current user id: id -un
	//  2. Find user's line in /etc/passwd by grep
	//  3. Extract default user's shell by cutting seven's column separated by :
	//  4. Execute the shell path with eval
	if err := c.Exec([]string{"/bin/sh", "-c", "printf '\\e[0m\\e[?25h' && clear && eval `grep ^$(id -un): /etc/passwd | cut -d : -f 7-`"}); err != nil {
		log.StatusErr(err)
	}

	return nil
}

func OpenInBrowser() MenuFn {
	c := cursor.Selected()
	if c == nil {
		return nil
	}

	webPort := c.Meta.Get("Web Port")
	if webPort == "" {
		return nil
	}
	link := "http://" + webPort + "/"
	browser.OpenURL(link)
	return nil
}

// Create a confirmation dialog with a given description string and
// func to perform if confirmed
func Confirm(txt string, fn func()) MenuFn {
	menu := func() MenuFn {
		ui.DefaultEvtStream.ResetHandlers()
		defer ui.DefaultEvtStream.ResetHandlers()

		m := menu.NewMenu()
		m.Selectable = true
		m.BorderLabel = "Confirm"
		m.SubText = txt

		items := []menu.Item{
			menu.Item{Val: "cancel", Label: "[c]ancel"},
			menu.Item{Val: "yes", Label: "[y]es"},
		}

		var response bool

		m.AddItems(items...)
		ui.Render(m)

		yes := func() {
			response = true
			ui.StopLoop()
		}

		no := func() {
			response = false
			ui.StopLoop()
		}

		HandleKeys("up", m.Up)
		HandleKeys("down", m.Down)
		HandleKeys("exit", no)
		ui.Handle("/sys/kbd/c", func(ui.Event) { no() })
		ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })

		ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
			switch m.SelectedValue() {
			case "cancel":
				no()
			case "yes":
				yes()
			}
		})

		ui.Loop()
		if response {
			fn()
		}
		return nil
	}
	return menu
}

type toggleLog struct {
	timestamp time.Time
	message   string
}

func (t *toggleLog) Toggle(on bool) string {
	if on {
		return fmt.Sprintf("%s %s", t.timestamp.Format("2006-01-02T15:04:05.999Z07:00"), t.message)
	}
	return t.message
}

func logReader(container *container.Container) (logs chan widgets.ToggleText, quit chan bool) {

	logCollector := container.Logs()
	stream := logCollector.Stream()
	logs = make(chan widgets.ToggleText)
	quit = make(chan bool)

	go func() {
		for {
			select {
			case log := <-stream:
				logs <- &toggleLog{timestamp: log.Timestamp, message: log.Message}
			case <-quit:
				logCollector.Stop()
				close(logs)
				return
			}
		}
	}()
	return
}

func confirmTxt(a, n string) string { return fmt.Sprintf("%s container %s?", a, n) }


================================================
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))
	}
}
Download .txt
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
Download .txt
SYMBOL INDEX (515 symbols across 60 files)

FILE: colors.go
  function InvertColorMap (line 53) | func InvertColorMap() {

FILE: config/columns.go
  type Column (line 81) | type Column struct
  function ColumnsString (line 88) | func ColumnsString() string { return strings.Join(EnabledColumns(), ",") }
  function EnabledColumns (line 91) | func EnabledColumns() (a []string) {
  function ColumnToggle (line 103) | func ColumnToggle(name string) {
  function ColumnLeft (line 110) | func ColumnLeft(name string) {
  function ColumnRight (line 118) | func ColumnRight(name string) {
  function SetColumns (line 126) | func SetColumns(names []string) {
  function swapCols (line 155) | func swapCols(i, j int) { GlobalColumns[i], GlobalColumns[j] = GlobalCol...
  function popColumn (line 157) | func popColumn(name string) *Column {
  function colIndex (line 168) | func colIndex(name string) int {

FILE: config/file.go
  type File (line 17) | type File struct
  function exportConfig (line 22) | func exportConfig() File {
  function Read (line 45) | func Read() error {
  function Write (line 79) | func Write() (path string, err error) {
  function getConfigPath (line 116) | func getConfigPath() (path string, err error) {
  function xdgSupport (line 137) | func xdgSupport() bool {

FILE: config/main.go
  function Init (line 19) | func Init() {
  function quote (line 35) | func quote(s string) string {
  function getEnv (line 40) | func getEnv(key, defaultVal string) string {

FILE: config/param.go
  type Param (line 22) | type Param struct
  function Get (line 29) | func Get(k string) *Param {
  function GetVal (line 42) | func GetVal(k string) string {
  function Update (line 47) | func Update(k, v string) {

FILE: config/switch.go
  type Switch (line 27) | type Switch struct
  function GetSwitch (line 34) | func GetSwitch(k string) *Switch {
  function GetSwitchVal (line 47) | func GetSwitchVal(k string) bool {
  function UpdateSwitch (line 51) | func UpdateSwitch(k string, val bool) {
  function Toggle (line 64) | func Toggle(k string) {

FILE: connector/collector/docker.go
  type Docker (line 9) | type Docker struct
    method Start (line 28) | func (c *Docker) Start() {
    method Running (line 60) | func (c *Docker) Running() bool {
    method Stream (line 64) | func (c *Docker) Stream() chan models.Metrics {
    method Logs (line 68) | func (c *Docker) Logs() LogCollector {
    method Stop (line 73) | func (c *Docker) Stop() {
    method ReadCPU (line 78) | func (c *Docker) ReadCPU(stats *api.Stats) {
    method ReadMem (line 96) | func (c *Docker) ReadMem(stats *api.Stats) {
    method ReadNet (line 102) | func (c *Docker) ReadNet(stats *api.Stats) {
    method ReadIO (line 111) | func (c *Docker) ReadIO(stats *api.Stats) {
  function NewDocker (line 20) | func NewDocker(client *api.Client, id string) *Docker {

FILE: connector/collector/docker_logs.go
  type DockerLogs (line 14) | type DockerLogs struct
    method Stream (line 28) | func (l *DockerLogs) Stream() chan models.Log {
    method Stop (line 80) | func (l *DockerLogs) Stop() { l.done <- true }
    method parseTime (line 82) | func (l *DockerLogs) parseTime(s string) time.Time {
    method stripPfx (line 99) | func (l *DockerLogs) stripPfx(s string) string {
  function NewDockerLogs (line 20) | func NewDockerLogs(id string, client *api.Client) *DockerLogs {

FILE: connector/collector/main.go
  type LogCollector (line 12) | type LogCollector interface
  type Collector (line 17) | type Collector interface
  function round (line 25) | func round(num float64) int {
  function percent (line 30) | func percent(val float64, total float64) int {

FILE: connector/collector/mock.go
  type Mock (line 14) | type Mock struct
    method Running (line 31) | func (c *Mock) Running() bool {
    method Start (line 35) | func (c *Mock) Start() {
    method Stop (line 41) | func (c *Mock) Stop() {
    method Stream (line 46) | func (c *Mock) Stream() chan models.Metrics {
    method Logs (line 50) | func (c *Mock) Logs() LogCollector {
    method run (line 54) | func (c *Mock) run() {
  function NewMock (line 22) | func NewMock(a int64) *Mock {

FILE: connector/collector/mock_logs.go
  constant mockLog (line 9) | mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea...
  type MockLogs (line 11) | type MockLogs struct
    method Stream (line 15) | func (l *MockLogs) Stream() chan models.Log {
    method Stop (line 31) | func (l *MockLogs) Stop() { l.done <- true }

FILE: connector/collector/proc.go
  constant clockTicksPerSecond (line 13) | clockTicksPerSecond  uint64 = 100
  constant nanoSecondsPerSecond (line 14) | nanoSecondsPerSecond        = 1e9
  function getSysMemTotal (line 17) | func getSysMemTotal() int64 {
  function getSysCPUUsage (line 27) | func getSysCPUUsage() uint64 {

FILE: connector/collector/runc.go
  type Runc (line 17) | type Runc struct
    method Running (line 39) | func (c *Runc) Running() bool {
    method Start (line 43) | func (c *Runc) Start() {
    method Stop (line 49) | func (c *Runc) Stop() {
    method Stream (line 54) | func (c *Runc) Stream() chan models.Metrics {
    method Logs (line 58) | func (c *Runc) Logs() LogCollector {
    method run (line 62) | func (c *Runc) run() {
    method ReadCPU (line 88) | func (c *Runc) ReadCPU(stats *cgroups.Stats) {
    method ReadMem (line 104) | func (c *Runc) ReadMem(stats *cgroups.Stats) {
    method ReadNet (line 113) | func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {
    method ReadIO (line 122) | func (c *Runc) ReadIO(stats *cgroups.Stats) {
  function NewRunc (line 29) | func NewRunc(libc libcontainer.Container) *Runc {

FILE: connector/docker.go
  function init (line 18) | func init() { enabled["docker"] = NewDocker }
  type StatusUpdate (line 28) | type StatusUpdate struct
  type Docker (line 34) | type Docker struct
    method Wait (line 78) | func (cm *Docker) Wait() struct{} { return <-cm.closed }
    method watchEvents (line 81) | func (cm *Docker) watchEvents() {
    method refresh (line 173) | func (cm *Docker) refresh(c *container.Container) {
    method inspect (line 198) | func (cm *Docker) inspect(id string) (insp *api.Container, found bool,...
    method refreshAll (line 221) | func (cm *Docker) refreshAll() {
    method Loop (line 237) | func (cm *Docker) Loop() {
    method LoopStatuses (line 249) | func (cm *Docker) LoopStatuses() {
    method MustGet (line 268) | func (cm *Docker) MustGet(id string) *container.Container {
    method Get (line 286) | func (cm *Docker) Get(id string) (*container.Container, bool) {
    method delByID (line 294) | func (cm *Docker) delByID(id string) {
    method All (line 302) | func (cm *Docker) All() (containers container.Containers) {
  function NewDocker (line 43) | func NewDocker() (Connector, error) {
  function portsFormat (line 127) | func portsFormat(ports map[api.Port][]api.PortBinding) string {
  function webPort (line 145) | func webPort(ports map[api.Port][]api.PortBinding) string {
  function ipsFormat (line 162) | func ipsFormat(networks map[string]api.ContainerNetwork) string {
  function calcUptime (line 211) | func calcUptime(insp *api.Container) string {
  function shortName (line 315) | func shortName(name string) string {

FILE: connector/main.go
  type ConnectorFn (line 18) | type ConnectorFn
  type Connector (line 20) | type Connector interface
  type ConnectorSuper (line 31) | type ConnectorSuper struct
    method Get (line 49) | func (cs *ConnectorSuper) Get() (Connector, error) {
    method setError (line 58) | func (cs *ConnectorSuper) setError(err error) {
    method loop (line 64) | func (cs *ConnectorSuper) loop() {
  function NewConnectorSuper (line 38) | func NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {
  function Enabled (line 89) | func Enabled() (a []string) {
  function ByName (line 99) | func ByName(s string) (*ConnectorSuper, error) {

FILE: connector/manager/docker.go
  type Docker (line 11) | type Docker struct
    method Exec (line 83) | func (dc *Docker) Exec(cmd []string) error {
    method Start (line 105) | func (dc *Docker) Start() error {
    method Stop (line 117) | func (dc *Docker) Stop() error {
    method Remove (line 124) | func (dc *Docker) Remove() error {
    method Pause (line 131) | func (dc *Docker) Pause() error {
    method Unpause (line 138) | func (dc *Docker) Unpause() error {
    method Restart (line 145) | func (dc *Docker) Restart() error {
  function NewDocker (line 16) | func NewDocker(client *api.Client, id string) *Docker {
  type noClosableReader (line 24) | type noClosableReader struct
    method Read (line 28) | func (w *noClosableReader) Read(p []byte) (n int, err error) {
  constant STDIN (line 33) | STDIN  = 0
  constant STDOUT (line 34) | STDOUT = 1
  constant STDERR (line 35) | STDERR = 2
  type frameWriter (line 48) | type frameWriter struct
    method Write (line 54) | func (w *frameWriter) Write(p []byte) (n int, err error) {

FILE: connector/manager/main.go
  type Manager (line 7) | type Manager interface

FILE: connector/manager/mock.go
  type Mock (line 3) | type Mock struct
    method Start (line 9) | func (m *Mock) Start() error {
    method Stop (line 13) | func (m *Mock) Stop() error {
    method Remove (line 17) | func (m *Mock) Remove() error {
    method Pause (line 21) | func (m *Mock) Pause() error {
    method Unpause (line 25) | func (m *Mock) Unpause() error {
    method Restart (line 29) | func (m *Mock) Restart() error {
    method Exec (line 33) | func (m *Mock) Exec(cmd []string) error {
  function NewMock (line 5) | func NewMock() *Mock {

FILE: connector/manager/runc.go
  type Runc (line 3) | type Runc struct
    method Start (line 9) | func (rc *Runc) Start() error {
    method Stop (line 13) | func (rc *Runc) Stop() error {
    method Remove (line 17) | func (rc *Runc) Remove() error {
    method Pause (line 21) | func (rc *Runc) Pause() error {
    method Unpause (line 25) | func (rc *Runc) Unpause() error {
    method Restart (line 29) | func (rc *Runc) Restart() error {
    method Exec (line 33) | func (rc *Runc) Exec(cmd []string) error {
  function NewRunc (line 5) | func NewRunc() *Runc {

FILE: connector/mock.go
  function init (line 18) | func init() { enabled["mock"] = NewMock }
  type Mock (line 20) | type Mock struct
    method Init (line 32) | func (cs *Mock) Init() {
    method Wait (line 45) | func (cs *Mock) Wait() struct{} {
    method makeContainer (line 56) | func (cs *Mock) makeContainer(aggression int64, health bool) {
    method Loop (line 79) | func (cs *Mock) Loop() {
    method Get (line 93) | func (cs *Mock) Get(id string) (*container.Container, bool) {
    method All (line 103) | func (cs *Mock) All() container.Containers {
    method delByID (line 110) | func (cs *Mock) delByID(id string) {
    method del (line 120) | func (cs *Mock) del(idx ...int) {
  function NewMock (line 24) | func NewMock() (Connector, error) {
  function makeID (line 127) | func makeID() string {
  function makeName (line 135) | func makeName() string {
  function makeState (line 147) | func makeState() string {

FILE: connector/runc.go
  function init (line 20) | func init() { enabled["runc"] = NewRunc }
  type RuncOpts (line 22) | type RuncOpts struct
  function NewRuncOpts (line 27) | func NewRuncOpts() (RuncOpts, error) {
  type Runc (line 52) | type Runc struct
    method GetLibc (line 97) | func (cm *Runc) GetLibc(id string) libcontainer.Container {
    method refresh (line 118) | func (cm *Runc) refresh(id string) {
    method refreshAll (line 151) | func (cm *Runc) refreshAll() {
    method Loop (line 178) | func (cm *Runc) Loop() {
    method MustGet (line 185) | func (cm *Runc) MustGet(id string) *container.Container {
    method delByID (line 216) | func (cm *Runc) delByID(id string) {
    method Wait (line 225) | func (cm *Runc) Wait() struct{} { return <-cm.closed }
    method Get (line 228) | func (cm *Runc) Get(id string) (*container.Container, bool) {
    method All (line 236) | func (cm *Runc) All() (containers container.Containers) {
  function NewRunc (line 62) | func NewRunc() (Connector, error) {

FILE: container/main.go
  constant running (line 17) | running = "running"
  type Container (line 21) | type Container struct
    method RecreateWidgets (line 49) | func (c *Container) RecreateWidgets() {
    method SetUpdater (line 55) | func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
    method SetMeta (line 60) | func (c *Container) SetMeta(k, v string) {
    method GetMeta (line 65) | func (c *Container) GetMeta(k string) string {
    method SetState (line 69) | func (c *Container) SetState(s string) {
    method Logs (line 83) | func (c *Container) Logs() collector.LogCollector {
    method Read (line 88) | func (c *Container) Read(stream chan models.Metrics) {
    method Start (line 101) | func (c *Container) Start() {
    method Stop (line 112) | func (c *Container) Stop() {
    method Remove (line 123) | func (c *Container) Remove() {
    method Pause (line 130) | func (c *Container) Pause() {
    method Unpause (line 141) | func (c *Container) Unpause() {
    method Restart (line 152) | func (c *Container) Restart() {
    method Exec (line 162) | func (c *Container) Exec(cmd []string) error {
  function New (line 32) | func New(id string, collector collector.Collector, manager manager.Manag...

FILE: container/sort.go
  type sortMethod (line 11) | type sortMethod
  function SortFields (line 93) | func SortFields() (fields []string) {
  type Containers (line 100) | type Containers
    method Sort (line 102) | func (a Containers) Sort()         { sort.Sort(a) }
    method Len (line 103) | func (a Containers) Len() int      { return len(a) }
    method Swap (line 104) | func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
    method Less (line 105) | func (a Containers) Less(i, j int) bool {
    method Filter (line 113) | func (a Containers) Filter() {
  function sumNet (line 130) | func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
  function sumIO (line 132) | func sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }

FILE: cursor.go
  type GridCursor (line 11) | type GridCursor struct
    method Len (line 18) | func (gc *GridCursor) Len() int { return len(gc.filtered) }
    method Selected (line 20) | func (gc *GridCursor) Selected() *container.Container {
    method RefreshContainers (line 30) | func (gc *GridCursor) RefreshContainers() (bool, error) {
    method Reset (line 58) | func (gc *GridCursor) Reset() {
    method Idx (line 74) | func (gc *GridCursor) Idx() int {
    method ScrollPage (line 84) | func (gc *GridCursor) ScrollPage() {
    method Up (line 106) | func (gc *GridCursor) Up() {
    method Down (line 125) | func (gc *GridCursor) Down() {
    method PgUp (line 144) | func (gc *GridCursor) PgUp() {
    method PgDown (line 167) | func (gc *GridCursor) PgDown() {
    method pgCount (line 191) | func (gc *GridCursor) pgCount() int {

FILE: cwidgets/compact/column.go
  type NewCompactColFn (line 29) | type NewCompactColFn
  function newRowWidgets (line 31) | func newRowWidgets() []CompactCol {
  type CompactCol (line 46) | type CompactCol interface

FILE: cwidgets/compact/gauge.go
  type CPUCol (line 12) | type CPUCol struct
    method SetMetrics (line 25) | func (w *CPUCol) SetMetrics(m models.Metrics) {
  function NewCPUCol (line 17) | func NewCPUCol() CompactCol {
  function NewCpuScaledCol (line 21) | func NewCpuScaledCol() CompactCol {
  type MemCol (line 39) | type MemCol struct
    method SetMetrics (line 47) | func (w *MemCol) SetMetrics(m models.Metrics) {
  function NewMemCol (line 43) | func NewMemCol() CompactCol {
  type GaugeCol (line 53) | type GaugeCol struct
    method Reset (line 68) | func (w *GaugeCol) Reset() {
    method Buffer (line 73) | func (w *GaugeCol) Buffer() ui.Buffer {
    method SetMeta (line 85) | func (w *GaugeCol) SetMeta(models.Meta)       {}
    method SetMetrics (line 86) | func (w *GaugeCol) SetMetrics(models.Metrics) {}
    method Header (line 87) | func (w *GaugeCol) Header() string            { return w.header }
    method FixedWidth (line 88) | func (w *GaugeCol) FixedWidth() int           { return w.fWidth }
    method Highlight (line 91) | func (w *GaugeCol) Highlight() {
    method UnHighlight (line 97) | func (w *GaugeCol) UnHighlight() {
  function NewGaugeCol (line 59) | func NewGaugeCol(header string) *GaugeCol {
  function colorScale (line 102) | func colorScale(n int) ui.Attribute {

FILE: cwidgets/compact/grid.go
  type CompactGrid (line 7) | type CompactGrid struct
    method Align (line 24) | func (cg *CompactGrid) Align() {
    method Clear (line 40) | func (cg *CompactGrid) Clear() {
    method GetHeight (line 45) | func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.head...
    method SetX (line 46) | func (cg *CompactGrid) SetX(x int)     { cg.X = x }
    method SetY (line 47) | func (cg *CompactGrid) SetY(y int)     { cg.Y = y }
    method SetWidth (line 48) | func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
    method MaxRows (line 49) | func (cg *CompactGrid) MaxRows() int   { return ui.TermHeight() - cg.h...
    method calcWidths (line 52) | func (cg *CompactGrid) calcWidths() []int {
    method pageRows (line 75) | func (cg *CompactGrid) pageRows() (rows []RowBufferer) {
    method Buffer (line 81) | func (cg *CompactGrid) Buffer() ui.Buffer {
    method AddRows (line 89) | func (cg *CompactGrid) AddRows(rows ...RowBufferer) {
    method rebuildHeader (line 93) | func (cg *CompactGrid) rebuildHeader() {
  function NewCompactGrid (line 18) | func NewCompactGrid() *CompactGrid {

FILE: cwidgets/compact/header.go
  type CompactHeader (line 7) | type CompactHeader struct
    method GetHeight (line 23) | func (row *CompactHeader) GetHeight() int {
    method SetWidths (line 27) | func (row *CompactHeader) SetWidths(totalWidth int, widths []int) {
    method SetX (line 38) | func (row *CompactHeader) SetX(x int) {
    method SetY (line 42) | func (row *CompactHeader) SetY(y int) {
    method Buffer (line 49) | func (row *CompactHeader) Buffer() ui.Buffer {
    method clearFieldPars (line 57) | func (row *CompactHeader) clearFieldPars() {
    method addFieldPar (line 61) | func (row *CompactHeader) addFieldPar(s string) {
  function NewCompactHeader (line 16) | func NewCompactHeader() *CompactHeader {

FILE: cwidgets/compact/row.go
  constant rowPadding (line 11) | rowPadding = 1
  type RowBufferer (line 15) | type RowBufferer interface
  type CompactRow (line 22) | type CompactRow struct
    method SetMeta (line 41) | func (row *CompactRow) SetMeta(m models.Meta) {
    method SetMetrics (line 47) | func (row *CompactRow) SetMetrics(m models.Metrics) {
    method Reset (line 54) | func (row *CompactRow) Reset() {
    method GetHeight (line 60) | func (row *CompactRow) GetHeight() int { return row.Height }
    method SetY (line 64) | func (row *CompactRow) SetY(y int) {
    method SetWidths (line 76) | func (row *CompactRow) SetWidths(totalWidth int, widths []int) {
    method Buffer (line 89) | func (row *CompactRow) Buffer() ui.Buffer {
    method Highlight (line 98) | func (row *CompactRow) Highlight() {
    method UnHighlight (line 107) | func (row *CompactRow) UnHighlight() {
  function NewCompactRow (line 30) | func NewCompactRow() *CompactRow {
  type RowBg (line 116) | type RowBg struct
    method Highlight (line 128) | func (w *RowBg) Highlight()   { w.Bg = ui.ThemeAttr("par.text.fg") }
    method UnHighlight (line 129) | func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }
  function NewRowBg (line 120) | func NewRowBg() *RowBg {

FILE: cwidgets/compact/status.go
  type Status (line 10) | type Status struct
    method Buffer (line 27) | func (s *Status) Buffer() ui.Buffer {
    method SetMeta (line 34) | func (s *Status) SetMeta(m models.Meta) {
    method Reset (line 40) | func (s *Status) Reset()                    {}
    method SetMetrics (line 41) | func (s *Status) SetMetrics(models.Metrics) {}
    method Highlight (line 42) | func (s *Status) Highlight()                {}
    method UnHighlight (line 43) | func (s *Status) UnHighlight()              {}
    method Header (line 44) | func (s *Status) Header() string            { return "" }
    method FixedWidth (line 45) | func (s *Status) FixedWidth() int           { return 3 }
    method setState (line 47) | func (s *Status) setState(val string) {
    method setHealth (line 72) | func (s *Status) setHealth(val string) {
  function NewStatus (line 16) | func NewStatus() CompactCol {

FILE: cwidgets/compact/text.go
  type MetaCol (line 13) | type MetaCol struct
    method SetMeta (line 18) | func (w *MetaCol) SetMeta(m models.Meta) {
  function NewNameCol (line 22) | func NewNameCol() CompactCol {
  function NewCIDCol (line 28) | func NewCIDCol() CompactCol {
  function NewImageCol (line 34) | func NewImageCol() CompactCol {
  function NewPortsCol (line 38) | func NewPortsCol() CompactCol {
  function NewIpsCol (line 42) | func NewIpsCol() CompactCol {
  function NewCreatedCol (line 46) | func NewCreatedCol() CompactCol {
  type NetCol (line 52) | type NetCol struct
    method SetMetrics (line 60) | func (w *NetCol) SetMetrics(m models.Metrics) {
  function NewNetCol (line 56) | func NewNetCol() CompactCol {
  type IOCol (line 65) | type IOCol struct
    method SetMetrics (line 73) | func (w *IOCol) SetMetrics(m models.Metrics) {
  function NewIOCol (line 69) | func NewIOCol() CompactCol {
  type PIDCol (line 78) | type PIDCol struct
    method SetMetrics (line 88) | func (w *PIDCol) SetMetrics(m models.Metrics) {
  function NewPIDCol (line 82) | func NewPIDCol() CompactCol {
  type UptimeCol (line 92) | type UptimeCol struct
    method SetMeta (line 100) | func (w *UptimeCol) SetMeta(m models.Meta) {
  function NewUptimeCol (line 96) | func NewUptimeCol() CompactCol {
  type TextCol (line 104) | type TextCol struct
    method Highlight (line 123) | func (w *TextCol) Highlight() {
    method UnHighlight (line 129) | func (w *TextCol) UnHighlight() {
    method Reset (line 136) | func (w *TextCol) Reset()                    { w.setText("-") }
    method SetMeta (line 137) | func (w *TextCol) SetMeta(models.Meta)       {}
    method SetMetrics (line 138) | func (w *TextCol) SetMetrics(models.Metrics) {}
    method Header (line 139) | func (w *TextCol) Header() string            { return w.header }
    method FixedWidth (line 140) | func (w *TextCol) FixedWidth() int           { return w.fWidth }
    method setText (line 142) | func (w *TextCol) setText(s string) {
  function NewTextCol (line 110) | func NewTextCol(header string) *TextCol {

FILE: cwidgets/compact/util.go
  constant colSpacing (line 11) | colSpacing = 1
  function centerParText (line 13) | func centerParText(p *ui.Par) {

FILE: cwidgets/main.go
  type WidgetUpdater (line 10) | type WidgetUpdater interface
  type NullWidgetUpdater (line 15) | type NullWidgetUpdater struct
    method SetMeta (line 18) | func (wu NullWidgetUpdater) SetMeta(models.Meta) {}
    method SetMetrics (line 21) | func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}

FILE: cwidgets/single/cpu.go
  type Cpu (line 7) | type Cpu struct
    method Update (line 30) | func (w *Cpu) Update(val int) {
  function NewCpu (line 12) | func NewCpu() *Cpu {

FILE: cwidgets/single/env.go
  type Env (line 12) | type Env struct
    method Set (line 28) | func (w *Env) Set(allEnvs string) {
  function NewEnv (line 17) | func NewEnv() *Env {

FILE: cwidgets/single/hist.go
  type IntHist (line 3) | type IntHist struct
    method Append (line 16) | func (h *IntHist) Append(val int) {
  function NewIntHist (line 9) | func NewIntHist(max int) *IntHist {
  type DiffHist (line 24) | type DiffHist struct
    method Append (line 33) | func (h *DiffHist) Append(val int) {
  function NewDiffHist (line 29) | func NewDiffHist(max int) *DiffHist {
  type FloatHist (line 41) | type FloatHist struct
    method Append (line 54) | func (h FloatHist) Append(val float64) {
  function NewFloatHist (line 47) | func NewFloatHist(max int) FloatHist {

FILE: cwidgets/single/info.go
  type Info (line 11) | type Info struct
    method Set (line 26) | func (w *Info) Set(k, v string) {
  function NewInfo (line 16) | func NewInfo() *Info {
  function mkInfoRows (line 41) | func mkInfoRows(k, v string) (rows [][]string) {

FILE: cwidgets/single/io.go
  type IO (line 11) | type IO struct
    method Update (line 41) | func (w *IO) Update(read int64, write int64) {
  function NewIO (line 17) | func NewIO() *IO {

FILE: cwidgets/single/logs.go
  type LogLines (line 10) | type LogLines struct
    method tail (line 23) | func (ll *LogLines) tail(n int) []string {
    method getLines (line 30) | func (ll *LogLines) getLines(start, end int) []string {
    method add (line 37) | func (ll *LogLines) add(l models.Log) {
  function NewLogLines (line 15) | func NewLogLines(max int) *LogLines {
  type Logs (line 47) | type Logs struct
    method Align (line 70) | func (w *Logs) Align() {
    method Buffer (line 75) | func (w *Logs) Buffer() ui.Buffer {
    method lineHeight (line 83) | func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidt...
  function NewLogs (line 52) | func NewLogs(stream chan models.Log) *Logs {

FILE: cwidgets/single/main.go
  type Single (line 15) | type Single struct
    method Up (line 38) | func (e *Single) Up() {
    method Down (line 46) | func (e *Single) Down() {
    method SetWidth (line 54) | func (e *Single) SetWidth(w int) { e.Width = w }
    method SetMeta (line 55) | func (e *Single) SetMeta(m models.Meta) {
    method SetMetrics (line 65) | func (e *Single) SetMetrics(m models.Metrics) {
    method GetHeight (line 73) | func (e *Single) GetHeight() (h int) {
    method Align (line 83) | func (e *Single) Align() {
    method Buffer (line 102) | func (e *Single) Buffer() ui.Buffer {
    method all (line 118) | func (e *Single) all() []ui.GridBufferer {
  function NewSingle (line 26) | func NewSingle() *Single {
  function termSizeError (line 129) | func termSizeError() *ui.Par {

FILE: cwidgets/single/mem.go
  type Mem (line 10) | type Mem struct
    method Align (line 37) | func (w *Mem) Align() {
    method Buffer (line 46) | func (w *Mem) Buffer() ui.Buffer {
    method Update (line 78) | func (w *Mem) Update(val int, limit int) {
  function NewMem (line 18) | func NewMem() *Mem {
  function newMemLabel (line 54) | func newMemLabel() *ui.Par {
  function newMemChart (line 63) | func newMemChart() *ui.MBarChart {

FILE: cwidgets/single/net.go
  type Net (line 11) | type Net struct
    method Update (line 41) | func (w *Net) Update(rx int64, tx int64) {
  function NewNet (line 17) | func NewNet() *Net {

FILE: cwidgets/util.go
  constant _ (line 9) | _           = iota
  constant kib (line 10) | kib float64 = 1 << (10 * iota)
  constant mib (line 11) | mib
  constant gib (line 12) | gib
  constant tib (line 13) | tib
  constant pib (line 14) | pib
  function ByteFormat (line 39) | func ByteFormat(n int) string          { return byteFormat(float64(n), f...
  function ByteFormatShort (line 40) | func ByteFormatShort(n int) string     { return byteFormat(float64(n), t...
  function ByteFormat64 (line 41) | func ByteFormat64(n int64) string      { return byteFormat(float64(n), f...
  function ByteFormat64Short (line 42) | func ByteFormat64Short(n int64) string { return byteFormat(float64(n), t...
  function byteFormat (line 44) | func byteFormat(n float64, short bool) string {
  function unpadFloat (line 61) | func unpadFloat(f float64, maxp int) string {
  function getPrecision (line 65) | func getPrecision(f float64, maxp int) int {

FILE: debug.go
  function logEvent (line 14) | func logEvent(e ui.Event) {
  function runtimeStats (line 29) | func runtimeStats() {
  function runtimeStack (line 39) | func runtimeStack() {
  function dumpContainer (line 46) | func dumpContainer(c *container.Container) {
  function inspect (line 55) | func inspect(i interface{}) (s string) {
  function quote (line 69) | func quote(s string) string {

FILE: grid.go
  function ShowConnError (line 9) | func ShowConnError(err error) (exit bool) {
  function RedrawRows (line 47) | func RedrawRows(clr bool) {
  function SingleView (line 76) | func SingleView() MenuFn {
  function RefreshDisplay (line 108) | func RefreshDisplay() error {
  function Display (line 120) | func Display() bool {

FILE: keys.go
  function HandleKeys (line 37) | func HandleKeys(i string, f func()) {

FILE: logging/main.go
  constant size (line 12) | size = 1024
  type statusMsg (line 24) | type statusMsg struct
  type CTopLogger (line 29) | type CTopLogger struct
    method FlushStatus (line 36) | func (c *CTopLogger) FlushStatus() chan statusMsg {
    method StatusQueued (line 48) | func (c *CTopLogger) StatusQueued() bool     { return len(c.sLog) > 0 }
    method Status (line 49) | func (c *CTopLogger) Status(s string)        { c.addStatus(statusMsg{s...
    method StatusErr (line 50) | func (c *CTopLogger) StatusErr(err error)    { c.addStatus(statusMsg{e...
    method addStatus (line 51) | func (c *CTopLogger) addStatus(sm statusMsg) { c.sLog = append(c.sLog,...
    method Statusf (line 53) | func (c *CTopLogger) Statusf(s string, a ...interface{}) { c.Status(fm...
    method tail (line 98) | func (log *CTopLogger) tail() chan string {
    method Exit (line 123) | func (log *CTopLogger) Exit() {
  function Init (line 55) | func Init() *CTopLogger {
  function debugMode (line 131) | func debugMode() bool       { return os.Getenv("CTOP_DEBUG") == "1" }
  function debugModeTCP (line 132) | func debugModeTCP() bool    { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
  function debugModeFile (line 133) | func debugModeFile() string { return os.Getenv("CTOP_DEBUG_FILE") }

FILE: logging/server.go
  constant socketPath (line 11) | socketPath = "./ctop.sock"
  constant socketAddr (line 12) | socketAddr = "0.0.0.0:9000"
  function getListener (line 20) | func getListener() net.Listener {
  function StartServer (line 34) | func StartServer() {
  function StopServer (line 53) | func StopServer() {
  function handler (line 60) | func handler(wc io.WriteCloser) {

FILE: main.go
  function main (line 35) | func main() {
  function Shutdown (line 118) | func Shutdown() {
  function validSort (line 127) | func validSort(s string) {
  function panicExit (line 134) | func panicExit() {
  function printHelp (line 150) | func printHelp() {

FILE: menus.go
  type MenuFn (line 17) | type MenuFn
  function HelpMenu (line 37) | func HelpMenu() MenuFn {
  function FilterMenu (line 56) | func FilterMenu() MenuFn {
  function SortMenu (line 89) | func SortMenu() MenuFn {
  function ColumnsMenu (line 120) | func ColumnsMenu() MenuFn {
  function ContainerMenu (line 203) | func ContainerMenu() MenuFn {
  function LogMenu (line 335) | func LogMenu() MenuFn {
  function ExecShell (line 364) | func ExecShell() MenuFn {
  function OpenInBrowser (line 389) | func OpenInBrowser() MenuFn {
  function Confirm (line 406) | func Confirm(txt string, fn func()) MenuFn {
  type toggleLog (line 460) | type toggleLog struct
    method Toggle (line 465) | func (t *toggleLog) Toggle(on bool) string {
  function logReader (line 472) | func logReader(container *container.Container) (logs chan widgets.Toggle...
  function confirmTxt (line 494) | func confirmTxt(a, n string) string { return fmt.Sprintf("%s container %...

FILE: models/main.go
  type Log (line 5) | type Log struct
  type Meta (line 10) | type Meta
    method Get (line 26) | func (m Meta) Get(k string) string {
  function NewMeta (line 14) | func NewMeta(kvs ...string) Meta {
  type Metrics (line 33) | type Metrics struct
  function NewMetrics (line 46) | func NewMetrics() Metrics {

FILE: widgets/error.go
  type ErrorView (line 11) | type ErrorView struct
    method Append (line 39) | func (w *ErrorView) Append(s string) {
    method Buffer (line 48) | func (w *ErrorView) Buffer() ui.Buffer {
    method Resize (line 57) | func (w *ErrorView) Resize() {
  function NewErrorView (line 16) | func NewErrorView() *ErrorView {

FILE: widgets/header.go
  type CTopHeader (line 10) | type CTopHeader struct
    method Buffer (line 26) | func (c *CTopHeader) Buffer() ui.Buffer {
    method Align (line 36) | func (c *CTopHeader) Align() {
    method Height (line 40) | func (c *CTopHeader) Height() int {
    method SetCount (line 61) | func (c *CTopHeader) SetCount(val int) {
    method SetFilter (line 65) | func (c *CTopHeader) SetFilter(val string) {
  function NewCTopHeader (line 17) | func NewCTopHeader() *CTopHeader {
  function headerBgBordered (line 44) | func headerBgBordered() *ui.Par {
  function headerBg (line 52) | func headerBg() *ui.Par {
  function timeStr (line 73) | func timeStr() string {
  function headerPar (line 78) | func headerPar(x int, s string) *ui.Par {

FILE: widgets/input.go
  type Padding (line 13) | type Padding
  type Input (line 15) | type Input struct
    method calcSize (line 41) | func (i *Input) calcSize() {
    method Buffer (line 46) | func (i *Input) Buffer() ui.Buffer {
    method Stream (line 61) | func (i *Input) Stream() chan string {
    method KeyPress (line 66) | func (i *Input) KeyPress(e ui.Event) {
    method InputHandlers (line 88) | func (i *Input) InputHandlers() {
  function NewInput (line 26) | func NewInput() *Input {

FILE: widgets/menu/items.go
  type Item (line 3) | type Item struct
    method Text (line 9) | func (m Item) Text() string {
  type Items (line 16) | type Items
    method Len (line 26) | func (m Items) Len() int      { return len(m) }
    method Swap (line 27) | func (m Items) Swap(a, b int) { m[a], m[b] = m[b], m[a] }
    method Less (line 28) | func (m Items) Less(a, b int) bool {
  function NewItems (line 18) | func NewItems(items ...Item) (mitems Items) {

FILE: widgets/menu/main.go
  type Padding (line 9) | type Padding
  type Menu (line 11) | type Menu struct
    method AddItems (line 39) | func (m *Menu) AddItems(items ...Item) {
    method DelItem (line 47) | func (m *Menu) DelItem(s string) (success bool) {
    method ClearItems (line 60) | func (m *Menu) ClearItems() {
    method SetCursor (line 65) | func (m *Menu) SetCursor(s string) (success bool) {
    method SetToolTip (line 76) | func (m *Menu) SetToolTip(lines ...string) {
    method SelectedItem (line 80) | func (m *Menu) SelectedItem() Item {
    method SelectedValue (line 84) | func (m *Menu) SelectedValue() string {
    method Buffer (line 88) | func (m *Menu) Buffer() ui.Buffer {
    method Up (line 124) | func (m *Menu) Up() {
    method Down (line 131) | func (m *Menu) Down() {
    method refresh (line 139) | func (m *Menu) refresh() {
    method calcSize (line 148) | func (m *Menu) calcSize() {
  function NewMenu (line 24) | func NewMenu() *Menu {

FILE: widgets/menu/tooltip.go
  type ToolTip (line 7) | type ToolTip struct
    method Buffer (line 30) | func (t *ToolTip) Buffer() ui.Buffer {
    method Align (line 49) | func (t *ToolTip) Align() {
  function NewToolTip (line 15) | func NewToolTip(lines ...string) *ToolTip {

FILE: widgets/status.go
  type StatusLine (line 12) | type StatusLine struct
    method Display (line 31) | func (sl *StatusLine) Display() {
    method Show (line 51) | func (sl *StatusLine) Show(s string) {
    method ShowErr (line 57) | func (sl *StatusLine) ShowErr(s string) {
    method Buffer (line 63) | func (sl *StatusLine) Buffer() ui.Buffer {
    method Align (line 70) | func (sl *StatusLine) Align() {
    method Height (line 78) | func (sl *StatusLine) Height() int { return statusHeight }
  function NewStatusLine (line 17) | func NewStatusLine() *StatusLine {
  function statusBg (line 80) | func statusBg() *ui.Par {

FILE: widgets/view.go
  type ToggleText (line 8) | type ToggleText interface
  type TextView (line 13) | type TextView struct
    method Resize (line 49) | func (t *TextView) Resize() {
    method Toggle (line 58) | func (t *TextView) Toggle() {
    method Buffer (line 63) | func (t *TextView) Buffer() ui.Buffer {
    method renderLoop (line 82) | func (t *TextView) renderLoop() {
    method readInputLoop (line 101) | func (t *TextView) readInputLoop() {
  function NewTextView (line 25) | func NewTextView(lines <-chan ToggleText) *TextView {
  function splitLine (line 111) | func splitLine(line string, lineSize int) []string {

FILE: widgets/view_test.go
  function TestSplitEmptyLine (line 5) | func TestSplitEmptyLine(t *testing.T) {
  function TestSplitLineShorterThanLimit (line 13) | func TestSplitLineShorterThanLimit(t *testing.T) {
  function TestSplitLineLongerThanLimit (line 21) | func TestSplitLineLongerThanLimit(t *testing.T) {
  function TestSplitLineSameAsLimit (line 29) | func TestSplitLineSameAsLimit(t *testing.T) {
Condensed preview — 75 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (184K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 558,
    "preview": "version: 2\njobs:\n  build:\n    working_directory: ~/build\n    docker:\n      - image: cimg/go:1.18\n    steps:\n      - chec"
  },
  {
    "path": ".gitignore",
    "chars": 25,
    "preview": "ctop\n.idea\n/vendor/\n*.log"
  },
  {
    "path": "Dockerfile",
    "chars": 273,
    "preview": "FROM quay.io/vektorcloud/go:1.18\n\nRUN apk add --no-cache make\n\nWORKDIR /app\nCOPY go.mod .\nRUN go mod download\n\nCOPY . .\n"
  },
  {
    "path": "LICENSE",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2017 VektorLab\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "Makefile",
    "chars": 1402,
    "preview": "NAME=ctop\nVERSION=$(shell cat VERSION)\nBUILD=$(shell git rev-parse --short HEAD)\nLD_FLAGS=\"-w -X main.version=$(VERSION)"
  },
  {
    "path": "README.md",
    "chars": 4741,
    "preview": "<p align=\"center\"><img width=\"200px\" src=\"/_docs/img/logo.png\" alt=\"ctop\"/></p>\n\n#\n\n![release][release] ![homebrew][home"
  },
  {
    "path": "VERSION",
    "chars": 6,
    "preview": "0.7.7\n"
  },
  {
    "path": "_docs/build.md",
    "chars": 322,
    "preview": "# Build\n\nTo build `ctop` from source, simply clone the repo and run:\n\n```bash\nmake build\n```\n\nTo build a minimal Docker "
  },
  {
    "path": "_docs/connectors.md",
    "chars": 777,
    "preview": "# Connectors\n\n`ctop` comes with the below native connectors, enabled via the `--connector` option.\n\nDefault connector be"
  },
  {
    "path": "_docs/debug.md",
    "chars": 2033,
    "preview": "# Debug Mode\n\n`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.\n\n"
  },
  {
    "path": "_docs/single.md",
    "chars": 167,
    "preview": "# Single Container View\n\nctop provides a rolling, single container view for following metrics\n<p align=\"center\"><img wid"
  },
  {
    "path": "_docs/status.md",
    "chars": 598,
    "preview": "# Status Indicator\n\nThe `ctop` grid view provides a compact status indicator to convey container state\n\n<img width=\"200p"
  },
  {
    "path": "colors.go",
    "chars": 1547,
    "preview": "package main\n\nimport (\n\t\"regexp\"\n\n\tui \"github.com/gizak/termui\"\n)\n\n/*\nValid colors:\n\tui.ColorDefault\n\tui.ColorBlack\n\tui."
  },
  {
    "path": "config/columns.go",
    "chars": 3372,
    "preview": "package config\n\nimport (\n\t\"strings\"\n)\n\n// defaults\nvar defaultColumns = []Column{\n\t{\n\t\tName:    \"status\",\n\t\tLabel:   \"St"
  },
  {
    "path": "config/file.go",
    "chars": 2713,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/BurntSushi/toml\"\n)\n\nvar (\n\txdg"
  },
  {
    "path": "config/main.go",
    "chars": 980,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/bcicen/ctop/logging\"\n)\n\nvar (\n\tGlobalParams   []*Param\n\tGlob"
  },
  {
    "path": "config/param.go",
    "chars": 936,
    "preview": "package config\n\n// defaults\nvar defaultParams = []*Param{\n\t&Param{\n\t\tKey:   \"filterStr\",\n\t\tVal:   \"\",\n\t\tLabel: \"Containe"
  },
  {
    "path": "config/switch.go",
    "chars": 1276,
    "preview": "package config\n\n// defaults\nvar defaultSwitches = []*Switch{\n\t&Switch{\n\t\tKey:   \"sortReversed\",\n\t\tVal:   false,\n\t\tLabel:"
  },
  {
    "path": "connector/collector/docker.go",
    "chars": 2542,
    "preview": "package collector\n\nimport (\n\t\"github.com/bcicen/ctop/models\"\n\tapi \"github.com/fsouza/go-dockerclient\"\n)\n\n// Docker colle"
  },
  {
    "path": "connector/collector/docker_logs.go",
    "chars": 2100,
    "preview": "package collector\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bcicen/ctop/models\"\n\tapi \"github."
  },
  {
    "path": "connector/collector/main.go",
    "chars": 534,
    "preview": "package collector\n\nimport (\n\t\"math\"\n\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n)\n\nvar log = log"
  },
  {
    "path": "connector/collector/mock.go",
    "chars": 1493,
    "preview": "//go:build !release\n// +build !release\n\npackage collector\n\nimport (\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/bcicen/ctop/model"
  },
  {
    "path": "connector/collector/mock_logs.go",
    "chars": 719,
    "preview": "package collector\n\nimport (\n\t\"time\"\n\n\t\"github.com/bcicen/ctop/models\"\n)\n\nconst mockLog = \"Cura ob pro qui tibi inveni du"
  },
  {
    "path": "connector/collector/proc.go",
    "chars": 976,
    "preview": "//go:build linux\n// +build linux\n\npackage collector\n\nimport (\n\tlinuxproc \"github.com/c9s/goprocinfo/linux\"\n)\n\nvar sysMem"
  },
  {
    "path": "connector/collector/runc.go",
    "chars": 2675,
    "preview": "//go:build linux\n// +build linux\n\npackage collector\n\nimport (\n\t\"time\"\n\n\t\"github.com/opencontainers/runc/libcontainer\"\n\t\""
  },
  {
    "path": "connector/docker.go",
    "chars": 7834,
    "preview": "package connector\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/op/go-logging\"\n\t\"github.com/hako/durafmt\"\n\n\t"
  },
  {
    "path": "connector/main.go",
    "chars": 2388,
    "preview": "package connector\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bcicen/ctop/container\"\n\t\"github.com/bcicen/ctop"
  },
  {
    "path": "connector/manager/docker.go",
    "chars": 3231,
    "preview": "package manager\n\nimport (\n\t\"fmt\"\n\tapi \"github.com/fsouza/go-dockerclient\"\n\t\"github.com/pkg/errors\"\n\t\"io\"\n\t\"os\"\n)\n\ntype D"
  },
  {
    "path": "connector/manager/main.go",
    "chars": 242,
    "preview": "package manager\n\nimport \"errors\"\n\nvar ActionNotImplErr = errors.New(\"action not implemented\")\n\ntype Manager interface {\n"
  },
  {
    "path": "connector/manager/mock.go",
    "chars": 506,
    "preview": "package manager\n\ntype Mock struct{}\n\nfunc NewMock() *Mock {\n\treturn &Mock{}\n}\n\nfunc (m *Mock) Start() error {\n\treturn Ac"
  },
  {
    "path": "connector/manager/runc.go",
    "chars": 513,
    "preview": "package manager\n\ntype Runc struct{}\n\nfunc NewRunc() *Runc {\n\treturn &Runc{}\n}\n\nfunc (rc *Runc) Start() error {\n\treturn A"
  },
  {
    "path": "connector/mock.go",
    "chars": 2947,
    "preview": "//go:build !release\n// +build !release\n\npackage connector\n\nimport (\n\t\"math/rand\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bcicen"
  },
  {
    "path": "connector/runc.go",
    "chars": 5114,
    "preview": "//go:build linux\n// +build linux\n\npackage connector\n\nimport (\n\t\"errors\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"ti"
  },
  {
    "path": "container/main.go",
    "chars": 3550,
    "preview": "package container\n\nimport (\n\t\"github.com/bcicen/ctop/connector/collector\"\n\t\"github.com/bcicen/ctop/connector/manager\"\n\t\""
  },
  {
    "path": "container/sort.go",
    "chars": 3204,
    "preview": "package container\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\n\t\"github.com/bcicen/ctop/config\"\n)\n\ntype sortMethod func(c1, c2 *C"
  },
  {
    "path": "cursor.go",
    "chars": 3789,
    "preview": "package main\n\nimport (\n\t\"math\"\n\n\t\"github.com/bcicen/ctop/connector\"\n\t\"github.com/bcicen/ctop/container\"\n\tui \"github.com/"
  },
  {
    "path": "cwidgets/compact/column.go",
    "chars": 1111,
    "preview": "package compact\n\nimport (\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/gizak/termu"
  },
  {
    "path": "cwidgets/compact/gauge.go",
    "chars": 2210,
    "preview": "package compact\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bcicen/ctop/cwidgets\"\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/g"
  },
  {
    "path": "cwidgets/compact/grid.go",
    "chars": 2134,
    "preview": "package compact\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype CompactGrid struct {\n\tui.GridBufferer\n\theader *CompactHe"
  },
  {
    "path": "cwidgets/compact/header.go",
    "chars": 1069,
    "preview": "package compact\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype CompactHeader struct {\n\tX, Y   int\n\tWidth  int\n\tHeight i"
  },
  {
    "path": "cwidgets/compact/row.go",
    "chars": 2224,
    "preview": "package compact\n\nimport (\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/mo"
  },
  {
    "path": "cwidgets/compact/status.go",
    "chars": 1894,
    "preview": "package compact\n\nimport (\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/gizak/termui\"\n)\n\n// Status indicator\ntype St"
  },
  {
    "path": "cwidgets/compact/text.go",
    "chars": 2937,
    "preview": "package compact\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bcicen/ctop/cwidgets\"\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/g"
  },
  {
    "path": "cwidgets/compact/util.go",
    "chars": 453,
    "preview": "package compact\n\n// Common helper functions\n\nimport (\n\t\"fmt\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nconst colSpacing = 1\n\nfun"
  },
  {
    "path": "cwidgets/main.go",
    "chars": 443,
    "preview": "package cwidgets\n\nimport (\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n)\n\nvar log = logging.Init("
  },
  {
    "path": "cwidgets/single/cpu.go",
    "chars": 541,
    "preview": "package single\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype Cpu struct {\n\t*ui.LineChart\n\thist FloatHist\n}\n\nfunc NewCp"
  },
  {
    "path": "cwidgets/single/env.go",
    "chars": 769,
    "preview": "package single\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar envPattern = regexp.MustCompile(`(?P"
  },
  {
    "path": "cwidgets/single/hist.go",
    "chars": 1154,
    "preview": "package single\n\ntype IntHist struct {\n\tVal    int   // most current data point\n\tData   []int // historical data points\n\t"
  },
  {
    "path": "cwidgets/single/info.go",
    "chars": 1068,
    "preview": "package single\n\nimport (\n\t\"strings\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar displayInfo = []string{\"id\", \"name\", \"image\", "
  },
  {
    "path": "cwidgets/single/io.go",
    "chars": 1046,
    "preview": "package single\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/bcicen/ctop/cwidgets\"\n\tui \"github.com/gizak/termui\"\n)\n\ntype IO "
  },
  {
    "path": "cwidgets/single/logs.go",
    "chars": 1656,
    "preview": "package single\n\nimport (\n\t\"time\"\n\n\t\"github.com/bcicen/ctop/models\"\n\tui \"github.com/gizak/termui\"\n)\n\ntype LogLines struct"
  },
  {
    "path": "cwidgets/single/main.go",
    "chars": 2312,
    "preview": "package single\n\nimport (\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n\tui \"github.com/gizak/termui"
  },
  {
    "path": "cwidgets/single/mem.go",
    "chars": 1597,
    "preview": "package single\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bcicen/ctop/cwidgets\"\n\tui \"github.com/gizak/termui\"\n)\n\ntype Mem struct {\n\t"
  },
  {
    "path": "cwidgets/single/net.go",
    "chars": 981,
    "preview": "package single\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/bcicen/ctop/cwidgets\"\n\tui \"github.com/gizak/termui\"\n)\n\ntype Net"
  },
  {
    "path": "cwidgets/util.go",
    "chars": 1311,
    "preview": "package cwidgets\n\nimport (\n\t\"strconv\"\n)\n\nconst (\n\t// byte ratio constants\n\t_           = iota\n\tkib float64 = 1 << (10 * "
  },
  {
    "path": "debug.go",
    "chars": 1664,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"runtime\"\n\n\t\"github.com/bcicen/ctop/container\"\n\tui \"github.com/gizak/termui\"\n)"
  },
  {
    "path": "go.mod",
    "chars": 3213,
    "preview": "module github.com/bcicen/ctop\n\nrequire (\n\tgithub.com/BurntSushi/toml v0.3.1\n\tgithub.com/c9s/goprocinfo v0.0.0-2017060900"
  },
  {
    "path": "go.sum",
    "chars": 25340,
    "preview": "bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=\ncloud.google.co"
  },
  {
    "path": "grid.go",
    "chars": 4759,
    "preview": "package main\n\nimport (\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/cwidgets/single\"\n\tui \"github.com/gizak/"
  },
  {
    "path": "install.sh",
    "chars": 2024,
    "preview": "#!/usr/bin/env bash\n# a simple install script for ctop\n\nKERNEL=$(uname -s)\n\nfunction output() { echo -e \"\\033[32mctop-in"
  },
  {
    "path": "keys.go",
    "chars": 677,
    "preview": "package main\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\n// Common action keybindings\nvar keyMap = map[string][]string{\n\t"
  },
  {
    "path": "logging/main.go",
    "chars": 2825,
    "preview": "package logging\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/op/go-logging\"\n)\n\nconst (\n\tsize = 1024\n)\n\nvar (\n\tLog    *CT"
  },
  {
    "path": "logging/server.go",
    "chars": 1011,
    "preview": "package logging\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n)\n\nconst (\n\tsocketPath = \"./ctop.sock\"\n\tsocketAddr = \"0.0.0.0:9000"
  },
  {
    "path": "main.go",
    "chars": 3199,
    "preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/"
  },
  {
    "path": "menus.go",
    "chars": 10908,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/container\"\n\t"
  },
  {
    "path": "models/main.go",
    "chars": 909,
    "preview": "package models\n\nimport \"time\"\n\ntype Log struct {\n\tTimestamp time.Time\n\tMessage   string\n}\n\ntype Meta map[string]string\n\n"
  },
  {
    "path": "widgets/error.go",
    "chars": 1301,
    "preview": "package widgets\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\tui \"github.com/gizak/termui\"\n)\n\ntype ErrorView struct {\n\t*ui.Par\n\t"
  },
  {
    "path": "widgets/header.go",
    "chars": 1575,
    "preview": "package widgets\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tui \"github.com/gizak/termui\"\n)\n\ntype CTopHeader struct {\n\tTime   *ui.Par\n\tCou"
  },
  {
    "path": "widgets/input.go",
    "chars": 1729,
    "preview": "package widgets\n\nimport (\n\t\"strings\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar (\n\tinput_chars = \"0123456789abcdefghijklmnopq"
  },
  {
    "path": "widgets/menu/items.go",
    "chars": 545,
    "preview": "package menu\n\ntype Item struct {\n\tVal   string\n\tLabel string\n}\n\n// Use label as display text of item, if given\nfunc (m I"
  },
  {
    "path": "widgets/menu/main.go",
    "chars": 3293,
    "preview": "package menu\n\nimport (\n\t\"sort\"\n\n\tui \"github.com/gizak/termui\"\n)\n\ntype Padding [2]int // x,y padding\n\ntype Menu struct {\n"
  },
  {
    "path": "widgets/menu/tooltip.go",
    "chars": 1083,
    "preview": "package menu\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype ToolTip struct {\n\tui.Block\n\tLines       []string\n\tTextFgCol"
  },
  {
    "path": "widgets/status.go",
    "chars": 1613,
    "preview": "package widgets\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\nvar (\n\tstatusHeight = 1\n\tstatusIter   = 3\n)\n\ntype StatusLine "
  },
  {
    "path": "widgets/view.go",
    "chars": 2749,
    "preview": "package widgets\n\nimport (\n\tui \"github.com/gizak/termui\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\ntype ToggleText interface {\n"
  },
  {
    "path": "widgets/view_test.go",
    "chars": 684,
    "preview": "package widgets\n\nimport \"testing\"\n\nfunc TestSplitEmptyLine(t *testing.T) {\n\n\tresult := splitLine(\"\", 5)\n\tif len(result) "
  }
]

About this extraction

This page contains the full source code of the bcicen/ctop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 75 files (161.3 KB), approximately 57.1k tokens, and a symbol index with 515 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!