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><ENTER></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:

================================================
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))
}
}
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
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.