[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\njobs:\n  build:\n    working_directory: ~/build\n    docker:\n      - image: cimg/go:1.18\n    steps:\n      - checkout\n      - setup_remote_docker:\n          version: 20.10.11\n      - run: make image\n      - deploy:\n          command: |\n            if [[ \"$CIRCLE_BRANCH\" == \"master\" ]]; then\n              docker tag ctop quay.io/vektorlab/ctop:latest\n              docker tag ctop quay.io/vektorlab/ctop:$(cat VERSION)\n              docker login -u $DOCKER_USER -p $DOCKER_PASS quay.io\n              docker push quay.io/vektorlab/ctop\n            fi\n"
  },
  {
    "path": ".gitignore",
    "content": "ctop\n.idea\n/vendor/\n*.log"
  },
  {
    "path": "Dockerfile",
    "content": "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 . .\nRUN make build && \\\n    mkdir -p /go/bin && \\\n    mv -v ctop /go/bin/\n\nFROM scratch\nENV TERM=linux\nCOPY --from=0 /go/bin/ctop /ctop\nENTRYPOINT [\"/ctop\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 VektorLab\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "Makefile",
    "content": "NAME=ctop\nVERSION=$(shell cat VERSION)\nBUILD=$(shell git rev-parse --short HEAD)\nLD_FLAGS=\"-w -X main.version=$(VERSION) -X main.build=$(BUILD)\"\n\nclean:\n\trm -rf _build/ release/\n\nbuild:\n\tgo mod download\n\tCGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop\n\nbuild-all:\n\tmkdir -p _build\n\tGOOS=darwin  GOARCH=amd64   CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64\n\tGOOS=linux   GOARCH=amd64   CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64\n\tGOOS=linux   GOARCH=arm     CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm\n\tGOOS=linux   GOARCH=arm64   CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64\n\tGOOS=linux   GOARCH=ppc64le CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-ppc64le\n\tGOOS=windows GOARCH=amd64   CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64\n\tcd _build; sha256sum * > sha256sums.txt\n\nrun-dev:\n\trm -f ctop.sock ctop\n\tgo build -ldflags $(LD_FLAGS) -o ctop\n\tCTOP_DEBUG=1 ./ctop\n\nimage:\n\tdocker build -t ctop -f Dockerfile .\n\nrelease:\n\tmkdir release\n\tcp _build/* release\n\tcd release; sha256sum --quiet --check sha256sums.txt && \\\n\tgh release create v$(VERSION) -d -t v$(VERSION) *\n\n.PHONY: build\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img width=\"200px\" src=\"/_docs/img/logo.png\" alt=\"ctop\"/></p>\n\n#\n\n![release][release] ![homebrew][homebrew] ![macports][macports] ![scoop][scoop]\n\nTop-like interface for container metrics\n\n`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:\n<p align=\"center\"><img src=\"_docs/img/grid.gif\" alt=\"ctop\"/></p>\n\nas well as a [single container view][single_view] for inspecting a specific container.\n\n`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.\n\n## Install\n\nFetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:\n\n#### Debian/Ubuntu\n\nMaintained by a [third party](https://packages.azlux.fr/)\n```bash\nsudo apt-get install ca-certificates curl gnupg lsb-release\ncurl -fsSL https://azlux.fr/repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/azlux-archive-keyring.gpg\necho \\\n  \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian \\\n  $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/azlux.list >/dev/null\nsudo apt-get update\nsudo apt-get install docker-ctop\n```\n\n#### Arch\n\n```bash\nsudo pacman -S ctop\n```\n\n_`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)_\n\n\n#### Linux (Generic)\n\n```bash\nsudo wget https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-linux-amd64 -O /usr/local/bin/ctop\nsudo chmod +x /usr/local/bin/ctop\n```\n\n#### OS X\n\n```bash\nbrew install ctop\n```\nor\n```bash\nsudo port install ctop\n```\nor\n```bash\nsudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-darwin-amd64\nsudo chmod +x /usr/local/bin/ctop\n```\n\n#### Windows\n\n`ctop` is available in [scoop](https://scoop.sh/):\n\n```powershell\nscoop install ctop\n```\n\n#### Docker\n\n```bash\ndocker run --rm -ti \\\n  --name=ctop \\\n  --volume /var/run/docker.sock:/var/run/docker.sock:ro \\\n  quay.io/vektorlab/ctop:latest\n```\n\n## Building\n\nBuild steps can be found [here][build].\n\n## Usage\n\n`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.\n\n### Config file\n\nWhile 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`).\n\nConfig file values will be loaded and applied the next time `ctop` is started.\n\n### Options\n\nOption | Description\n--- | ---\n`-a`\t| show active containers only\n`-f <string>` | set an initial filter string\n`-h`\t| display help dialog\n`-i`  | invert default colors\n`-r`\t| reverse container sort order\n`-s`  | select initial container sort field\n`-v`\t| output version information and exit\n\n### Keybindings\n\n|           Key            | Action                                                     |\n| :----------------------: | ---------------------------------------------------------- |\n| <kbd>&lt;ENTER&gt;</kbd> | Open container menu                                        |\n|       <kbd>a</kbd>       | Toggle display of all (running and non-running) containers |\n|       <kbd>f</kbd>       | Filter displayed containers (`esc` to clear when open)     |\n|       <kbd>H</kbd>       | Toggle ctop header                                         |\n|       <kbd>h</kbd>       | Open help dialog                                           |\n|       <kbd>s</kbd>       | Select container sort field                                |\n|       <kbd>r</kbd>       | Reverse container sort order                               |\n|       <kbd>o</kbd>       | Open single view                                           |\n|       <kbd>l</kbd>       | View container logs (`t` to toggle timestamp when open)    |\n|       <kbd>e</kbd>       | Exec Shell                                                 |\n|       <kbd>c</kbd>       | Configure columns                                          |\n|       <kbd>S</kbd>       | Save current configuration to file                         |\n|       <kbd>q</kbd>       | Quit ctop                                                  |\n\n[build]: _docs/build.md\n[connectors]: _docs/connectors.md\n[single_view]: _docs/single.md\n[release]: https://img.shields.io/github/release/bcicen/ctop.svg \"ctop\"\n[homebrew]: https://img.shields.io/homebrew/v/ctop.svg \"ctop\"\n[macports]: https://repology.org/badge/version-for-repo/macports/ctop.svg?header=macports \"ctop\"\n[scoop]: https://img.shields.io/scoop/v/ctop?bucket=main \"ctop\"\n\n## Alternatives\n\nSee [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker. \n"
  },
  {
    "path": "VERSION",
    "content": "0.7.7\n"
  },
  {
    "path": "_docs/build.md",
    "content": "# 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 image containing only `ctop`:\n```bash\nmake image\n```\n\nNow you can run your local image:\n\n```bash\ndocker run --rm -ti \\\n  --name ctop \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  ctop:latest\n```\n"
  },
  {
    "path": "_docs/connectors.md",
    "content": "# Connectors\n\n`ctop` comes with the below native connectors, enabled via the `--connector` option.\n\nDefault connector behavior can be changed by setting the relevant environment variables.\n\n## Docker\n\nDefault connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)\n\n#### Options\n\nVar | Description\n--- | ---\nDOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`)\n\n## RunC\n\nUsing this connector requires full privileges to the local runC root dir of container state (default: `/run/runc`)\n\n#### Options\n\nVar | Description\n--- | ---\nRUNC_ROOT | path to runc root for container state (default: `/run/runc`)\nRUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups\n"
  },
  {
    "path": "_docs/debug.md",
    "content": "# Debug Mode\n\n`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.\n\n## Quick Start\n\nIf running `ctop` via Docker, debug logging can be most easily enabled as below:\n```bash\ndocker run -ti --rm \\\n           --name=ctop \\\n           -e CTOP_DEBUG=1 \\\n           -e CTOP_DEBUG_TCP=1 \\\n           -p 9000:9000 \\\n           -v /var/run/docker.sock:/var/run/docker.sock \\\n           quay.io/vektorlab/ctop:latest\n```\n\nLog messages can be followed by connecting to the default listen address:\n```bash\ncurl -s localhost:9000\n```\n\nexample output:\n```\n15:06:43.881 ▶ NOTI 002 logger initialized\n15:06:43.881 ▶ INFO 003 loaded config param: \"filterStr\": \"\"\n15:06:43.881 ▶ INFO 004 loaded config param: \"sortField\": \"state\"\n15:06:43.881 ▶ INFO 005 loaded config switch: \"sortReversed\": false\n15:06:43.881 ▶ INFO 006 loaded config switch: \"allContainers\": true\n15:06:43.881 ▶ INFO 007 loaded config switch: \"enableHeader\": true\n15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...\n...\n```\n\n## Unix Socket\n\nDebug mode is enabled via the `CTOP_DEBUG` environment variable:\n\n```bash\nCTOP_DEBUG=1 ./ctop\n```\n\nWhile `ctop` is running, you can connect to the logging socket via socat or similar tools:\n```bash\nsocat unix-connect:./ctop.sock stdio\n```\n\n## TCP Logging Socket\n\nIn lieu of using a local unix socket, TCP logging can be enabled via the `CTOP_DEBUG_TCP` environment variable:\n\n```bash\nCTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop\n```\n\nA TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)\n\n## Log to file\n\nYou can also log to a file by specifying `CTOP_DEBUG_FILE=/path/to/ctop.log` environment variable:\n```sh\nCTOP_DEBUG=1 CTOP_DEBUG_FILE=ctop.log ./ctop\n```\n\nThis is useful for GoLand to see logs right in debug panel: \n* Edit Run configuration \n* Go to Logs tab\n* Specify this log file in \"Log file to be shown in console\".\nThen during debugging you'll see the log tab in debug panel:\n\n![Debug in GoLand](img/goland_debug.png)\n"
  },
  {
    "path": "_docs/single.md",
    "content": "# Single Container View\n\nctop provides a rolling, single container view for following metrics\n<p align=\"center\"><img width=\"80%\" src=\"img/single.gif\" alt=\"ctop\"/></p>\n"
  },
  {
    "path": "_docs/status.md",
    "content": "# Status Indicator\n\nThe `ctop` grid view provides a compact status indicator to convey container state\n\n<img width=\"200px\" src=\"img/status.png\" alt=\"ctop\"/>\n\n### Status\n\n<span align=\"center\">\n\nAppearance | Description\n--- | ---\nred | container is stopped\ngreen | container is running\n▮▮ | container is paused\n\n</span>\n\n### Health\nIf the container is configured with a health check, a `+` will appear next to the indicator\n\n<span align=\"center\">\n\nAppearance | Description\n--- | ---\nred | health check in failed state\nyellow | health check in starting state\ngreen | health check in OK state\n\n</span>\n"
  },
  {
    "path": "colors.go",
    "content": "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.ColorRed\n\tui.ColorGreen\n\tui.ColorYellow\n\tui.ColorBlue\n\tui.ColorMagenta\n\tui.ColorCyan\n\tui.ColorWhite\n*/\n\nvar ColorMap = map[string]ui.Attribute{\n\t\"fg\":                 ui.ColorWhite,\n\t\"bg\":                 ui.ColorDefault,\n\t\"block.bg\":           ui.ColorDefault,\n\t\"border.bg\":          ui.ColorDefault,\n\t\"border.fg\":          ui.ColorWhite,\n\t\"label.bg\":           ui.ColorDefault,\n\t\"label.fg\":           ui.ColorGreen,\n\t\"menu.text.fg\":       ui.ColorWhite,\n\t\"menu.text.bg\":       ui.ColorDefault,\n\t\"menu.border.fg\":     ui.ColorCyan,\n\t\"menu.label.fg\":      ui.ColorGreen,\n\t\"header.fg\":          ui.ColorBlack,\n\t\"header.bg\":          ui.ColorWhite,\n\t\"gauge.bar.bg\":       ui.ColorGreen,\n\t\"gauge.percent.fg\":   ui.ColorWhite,\n\t\"linechart.axes.fg\":  ui.ColorDefault,\n\t\"linechart.line.fg\":  ui.ColorGreen,\n\t\"mbarchart.bar.bg\":   ui.ColorGreen,\n\t\"mbarchart.num.fg\":   ui.ColorWhite,\n\t\"mbarchart.text.fg\":  ui.ColorWhite,\n\t\"par.text.fg\":        ui.ColorWhite,\n\t\"par.text.bg\":        ui.ColorDefault,\n\t\"par.text.hi\":        ui.ColorBlack,\n\t\"sparkline.line.fg\":  ui.ColorGreen,\n\t\"sparkline.title.fg\": ui.ColorWhite,\n\t\"status.ok\":          ui.ColorGreen,\n\t\"status.warn\":        ui.ColorYellow,\n\t\"status.danger\":      ui.ColorRed,\n}\n\nfunc InvertColorMap() {\n\tre := regexp.MustCompile(\".*.fg\")\n\tfor k := range ColorMap {\n\t\tif re.FindAllString(k, 1) != nil {\n\t\t\tColorMap[k] = ui.ColorBlack\n\t\t}\n\t}\n\tColorMap[\"par.text.hi\"] = ui.ColorWhite\n}\n"
  },
  {
    "path": "config/columns.go",
    "content": "package config\n\nimport (\n\t\"strings\"\n)\n\n// defaults\nvar defaultColumns = []Column{\n\t{\n\t\tName:    \"status\",\n\t\tLabel:   \"Status Indicator\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"name\",\n\t\tLabel:   \"Container Name\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"id\",\n\t\tLabel:   \"Container ID\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"image\",\n\t\tLabel:   \"Image name\",\n\t\tEnabled: false,\n\t},\n\t{\n\t\tName:    \"ports\",\n\t\tLabel:   \"Exposed ports\",\n\t\tEnabled: false,\n\t},\n\t{\n\t\tName:    \"IPs\",\n\t\tLabel:   \"Exposed IPs\",\n\t\tEnabled: false,\n\t},\n\t{\n\t\tName:    \"created\",\n\t\tLabel:   \"Date created\",\n\t\tEnabled: false,\n\t},\n\t{\n\t\tName:    \"cpu\",\n\t\tLabel:   \"CPU Usage\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"cpus\",\n\t\tLabel:   \"CPU Usage (% of system total)\",\n\t\tEnabled: false,\n\t},\n\t{\n\t\tName:    \"mem\",\n\t\tLabel:   \"Memory Usage\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"net\",\n\t\tLabel:   \"Network RX/TX\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"io\",\n\t\tLabel:   \"Disk IO Read/Write\",\n\t\tEnabled: true,\n\t},\n\t{\n\t\tName:    \"pids\",\n\t\tLabel:   \"Container PID Count\",\n\t\tEnabled: true,\n\t},\n\tColumn{\n\t\tName:    \"uptime\",\n\t\tLabel:   \"Running uptime duration\",\n\t\tEnabled: true,\n\t},\n}\n\ntype Column struct {\n\tName    string\n\tLabel   string\n\tEnabled bool\n}\n\n// ColumnsString returns an ordered and comma-delimited string of currently enabled Columns\nfunc ColumnsString() string { return strings.Join(EnabledColumns(), \",\") }\n\n// EnabledColumns returns an ordered array of enabled column names\nfunc EnabledColumns() (a []string) {\n\tlock.RLock()\n\tdefer lock.RUnlock()\n\tfor _, col := range GlobalColumns {\n\t\tif col.Enabled {\n\t\t\ta = append(a, col.Name)\n\t\t}\n\t}\n\treturn a\n}\n\n// ColumnToggle toggles the enabled status of a given column name\nfunc ColumnToggle(name string) {\n\tcol := GlobalColumns[colIndex(name)]\n\tcol.Enabled = !col.Enabled\n\tlog.Noticef(\"config change [column-%s]: %t -> %t\", col.Name, !col.Enabled, col.Enabled)\n}\n\n// ColumnLeft moves the column with given name up one position, if possible\nfunc ColumnLeft(name string) {\n\tidx := colIndex(name)\n\tif idx > 0 {\n\t\tswapCols(idx, idx-1)\n\t}\n}\n\n// ColumnRight moves the column with given name up one position, if possible\nfunc ColumnRight(name string) {\n\tidx := colIndex(name)\n\tif idx < len(GlobalColumns)-1 {\n\t\tswapCols(idx, idx+1)\n\t}\n}\n\n// Set Column order and enabled status from one or more provided Column names\nfunc SetColumns(names []string) {\n\tvar (\n\t\tn          int\n\t\tcurColStr  = ColumnsString()\n\t\tnewColumns = make([]*Column, len(GlobalColumns))\n\t)\n\n\tlock.Lock()\n\n\t// add enabled columns by name\n\tfor _, name := range names {\n\t\tnewColumns[n] = popColumn(name)\n\t\tnewColumns[n].Enabled = true\n\t\tn++\n\t}\n\n\t// extend with omitted columns as disabled\n\tfor _, col := range GlobalColumns {\n\t\tnewColumns[n] = col\n\t\tnewColumns[n].Enabled = false\n\t\tn++\n\t}\n\n\tGlobalColumns = newColumns\n\tlock.Unlock()\n\n\tlog.Noticef(\"config change [columns]: %s -> %s\", curColStr, ColumnsString())\n}\n\nfunc swapCols(i, j int) { GlobalColumns[i], GlobalColumns[j] = GlobalColumns[j], GlobalColumns[i] }\n\nfunc popColumn(name string) *Column {\n\tidx := colIndex(name)\n\tif idx < 0 {\n\t\tpanic(\"no such column name: \" + name)\n\t}\n\tcol := GlobalColumns[idx]\n\tGlobalColumns = append(GlobalColumns[:idx], GlobalColumns[idx+1:]...)\n\treturn col\n}\n\n// return index of column with given name, if any\nfunc colIndex(name string) int {\n\tfor n, c := range GlobalColumns {\n\t\tif c.Name == name {\n\t\t\treturn n\n\t\t}\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "config/file.go",
    "content": "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\txdgRe = regexp.MustCompile(\"^XDG_*\")\n)\n\ntype File struct {\n\tOptions map[string]string `toml:\"options\"`\n\tToggles map[string]bool   `toml:\"toggles\"`\n}\n\nfunc exportConfig() File {\n\t// update columns param from working config\n\tUpdate(\"columns\", ColumnsString())\n\n\tlock.RLock()\n\tdefer lock.RUnlock()\n\n\tc := File{\n\t\tOptions: make(map[string]string),\n\t\tToggles: make(map[string]bool),\n\t}\n\n\tfor _, p := range GlobalParams {\n\t\tc.Options[p.Key] = p.Val\n\t}\n\tfor _, sw := range GlobalSwitches {\n\t\tc.Toggles[sw.Key] = sw.Val\n\t}\n\n\treturn c\n}\n\n//\nfunc Read() error {\n\tvar config File\n\n\tpath, err := getConfigPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := toml.DecodeFile(path, &config); err != nil {\n\t\treturn err\n\t}\n\tfor k, v := range config.Options {\n\t\tUpdate(k, v)\n\t}\n\tfor k, v := range config.Toggles {\n\t\tUpdateSwitch(k, v)\n\t}\n\n\t// set working column config, if provided\n\tcolStr := GetVal(\"columns\")\n\tif len(colStr) > 0 {\n\t\tvar colNames []string\n\t\tfor _, s := range strings.Split(colStr, \",\") {\n\t\t\ts = strings.TrimSpace(s)\n\t\t\tif s != \"\" {\n\t\t\t\tcolNames = append(colNames, s)\n\t\t\t}\n\t\t}\n\t\tSetColumns(colNames)\n\t}\n\n\treturn nil\n}\n\nfunc Write() (path string, err error) {\n\tpath, err = getConfigPath()\n\tif err != nil {\n\t\treturn path, err\n\t}\n\n\tcfgdir := filepath.Dir(path)\n\t// create config dir if not exist\n\tif _, err := os.Stat(cfgdir); err != nil {\n\t\terr = os.MkdirAll(cfgdir, 0755)\n\t\tif err != nil {\n\t\t\treturn path, fmt.Errorf(\"failed to create config dir [%s]: %s\", cfgdir, err)\n\t\t}\n\t}\n\n\t// remove prior to writing new file\n\tif err := os.Remove(path); err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn path, err\n\t\t}\n\t}\n\n\tfile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)\n\tif err != nil {\n\t\treturn path, fmt.Errorf(\"failed to open config for writing: %s\", err)\n\t}\n\n\twriter := toml.NewEncoder(file)\n\terr = writer.Encode(exportConfig())\n\tif err != nil {\n\t\treturn path, fmt.Errorf(\"failed to write config: %s\", err)\n\t}\n\n\treturn path, nil\n}\n\n// determine config path from environment\nfunc getConfigPath() (path string, err error) {\n\thomeDir, ok := os.LookupEnv(\"HOME\")\n\tif !ok {\n\t\treturn path, fmt.Errorf(\"$HOME not set\")\n\t}\n\n\t// use xdg config home if possible\n\tif xdgSupport() {\n\t\txdgHome, ok := os.LookupEnv(\"XDG_CONFIG_HOME\")\n\t\tif !ok {\n\t\t\txdgHome = fmt.Sprintf(\"%s/.config\", homeDir)\n\t\t}\n\t\tpath = fmt.Sprintf(\"%s/ctop/config\", xdgHome)\n\t} else {\n\t\tpath = fmt.Sprintf(\"%s/.ctop\", homeDir)\n\t}\n\n\treturn path, nil\n}\n\n// test for environemnt supporting XDG spec\nfunc xdgSupport() bool {\n\tfor _, e := range os.Environ() {\n\t\tif xdgRe.FindAllString(e, 1) != nil {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "config/main.go",
    "content": "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\tGlobalSwitches []*Switch\n\tGlobalColumns  []*Column\n\tlock           sync.RWMutex\n\tlog            = logging.Init()\n)\n\nfunc Init() {\n\tfor _, p := range defaultParams {\n\t\tGlobalParams = append(GlobalParams, p)\n\t\tlog.Infof(\"loaded default config param [%s]: %s\", quote(p.Key), quote(p.Val))\n\t}\n\tfor _, s := range defaultSwitches {\n\t\tGlobalSwitches = append(GlobalSwitches, s)\n\t\tlog.Infof(\"loaded default config switch [%s]: %t\", quote(s.Key), s.Val)\n\t}\n\tfor _, c := range defaultColumns {\n\t\tx := c\n\t\tGlobalColumns = append(GlobalColumns, &x)\n\t\tlog.Infof(\"loaded default widget config [%s]: %t\", quote(x.Name), x.Enabled)\n\t}\n}\n\nfunc quote(s string) string {\n\treturn fmt.Sprintf(\"\\\"%s\\\"\", s)\n}\n\n// Return env var value if set, else return defaultVal\nfunc getEnv(key, defaultVal string) string {\n\tval := os.Getenv(key)\n\tif val != \"\" {\n\t\treturn val\n\t}\n\treturn defaultVal\n}\n"
  },
  {
    "path": "config/param.go",
    "content": "package config\n\n// defaults\nvar defaultParams = []*Param{\n\t&Param{\n\t\tKey:   \"filterStr\",\n\t\tVal:   \"\",\n\t\tLabel: \"Container Name or ID Filter\",\n\t},\n\t&Param{\n\t\tKey:   \"sortField\",\n\t\tVal:   \"state\",\n\t\tLabel: \"Container Sort Field\",\n\t},\n\t&Param{\n\t\tKey:   \"columns\",\n\t\tVal:   \"status,name,id,cpu,mem,net,io,pids,uptime\",\n\t\tLabel: \"Enabled Columns\",\n\t},\n}\n\ntype Param struct {\n\tKey   string\n\tVal   string\n\tLabel string\n}\n\n// Get Param by key\nfunc Get(k string) *Param {\n\tlock.RLock()\n\tdefer lock.RUnlock()\n\n\tfor _, p := range GlobalParams {\n\t\tif p.Key == k {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn &Param{} // default\n}\n\n// GetVal gets Param value by key\nfunc GetVal(k string) string {\n\treturn Get(k).Val\n}\n\n// Set param value\nfunc Update(k, v string) {\n\tp := Get(k)\n\tlog.Noticef(\"config change [%s]: %s -> %s\", k, quote(p.Val), quote(v))\n\n\tlock.Lock()\n\tdefer lock.Unlock()\n\tp.Val = v\n\t// log.Errorf(\"ignoring update for non-existant parameter: %s\", k)\n}\n"
  },
  {
    "path": "config/switch.go",
    "content": "package config\n\n// defaults\nvar defaultSwitches = []*Switch{\n\t&Switch{\n\t\tKey:   \"sortReversed\",\n\t\tVal:   false,\n\t\tLabel: \"Reverse sort order\",\n\t},\n\t&Switch{\n\t\tKey:   \"allContainers\",\n\t\tVal:   true,\n\t\tLabel: \"Show all containers\",\n\t},\n\t&Switch{\n\t\tKey:   \"fullRowCursor\",\n\t\tVal:   true,\n\t\tLabel: \"Highlight entire cursor row (vs. name only)\",\n\t},\n\t&Switch{\n\t\tKey:   \"enableHeader\",\n\t\tVal:   true,\n\t\tLabel: \"Enable status header\",\n\t},\n}\n\ntype Switch struct {\n\tKey   string\n\tVal   bool\n\tLabel string\n}\n\n// GetSwitch returns Switch by key\nfunc GetSwitch(k string) *Switch {\n\tlock.RLock()\n\tdefer lock.RUnlock()\n\n\tfor _, sw := range GlobalSwitches {\n\t\tif sw.Key == k {\n\t\t\treturn sw\n\t\t}\n\t}\n\treturn &Switch{} // default\n}\n\n// GetSwitchVal returns Switch value by key\nfunc GetSwitchVal(k string) bool {\n\treturn GetSwitch(k).Val\n}\n\nfunc UpdateSwitch(k string, val bool) {\n\tsw := GetSwitch(k)\n\n\tlock.Lock()\n\tdefer lock.Unlock()\n\n\tif sw.Val != val {\n\t\tlog.Noticef(\"config change [%s]: %t -> %t\", k, sw.Val, val)\n\t\tsw.Val = val\n\t}\n}\n\n// Toggle a boolean switch\nfunc Toggle(k string) {\n\tsw := GetSwitch(k)\n\n\tlock.Lock()\n\tdefer lock.Unlock()\n\n\tsw.Val = !sw.Val\n\tlog.Noticef(\"config change [%s]: %t -> %t\", k, !sw.Val, sw.Val)\n\t//log.Errorf(\"ignoring toggle for non-existant switch: %s\", k)\n}\n"
  },
  {
    "path": "connector/collector/docker.go",
    "content": "package collector\n\nimport (\n\t\"github.com/bcicen/ctop/models\"\n\tapi \"github.com/fsouza/go-dockerclient\"\n)\n\n// Docker collector\ntype Docker struct {\n\tmodels.Metrics\n\tid         string\n\tclient     *api.Client\n\trunning    bool\n\tstream     chan models.Metrics\n\tdone       chan bool\n\tlastCpu    float64\n\tlastSysCpu float64\n}\n\nfunc NewDocker(client *api.Client, id string) *Docker {\n\treturn &Docker{\n\t\tMetrics: models.Metrics{},\n\t\tid:      id,\n\t\tclient:  client,\n\t}\n}\n\nfunc (c *Docker) Start() {\n\tc.done = make(chan bool)\n\tc.stream = make(chan models.Metrics)\n\tstats := make(chan *api.Stats)\n\n\tgo func() {\n\t\topts := api.StatsOptions{\n\t\t\tID:     c.id,\n\t\t\tStats:  stats,\n\t\t\tStream: true,\n\t\t\tDone:   c.done,\n\t\t}\n\t\tc.client.Stats(opts)\n\t\tc.running = false\n\t}()\n\n\tgo func() {\n\t\tdefer close(c.stream)\n\t\tfor s := range stats {\n\t\t\tc.ReadCPU(s)\n\t\t\tc.ReadMem(s)\n\t\t\tc.ReadNet(s)\n\t\t\tc.ReadIO(s)\n\t\t\tc.stream <- c.Metrics\n\t\t}\n\t\tlog.Infof(\"collector stopped for container: %s\", c.id)\n\t}()\n\n\tc.running = true\n\tlog.Infof(\"collector started for container: %s\", c.id)\n}\n\nfunc (c *Docker) Running() bool {\n\treturn c.running\n}\n\nfunc (c *Docker) Stream() chan models.Metrics {\n\treturn c.stream\n}\n\nfunc (c *Docker) Logs() LogCollector {\n\treturn NewDockerLogs(c.id, c.client)\n}\n\n// Stop collector\nfunc (c *Docker) Stop() {\n\tc.running = false\n\tc.done <- true\n}\n\nfunc (c *Docker) ReadCPU(stats *api.Stats) {\n\tncpus := uint8(stats.CPUStats.OnlineCPUs)\n\tif ncpus == 0 {\n\t\tncpus = uint8(len(stats.CPUStats.CPUUsage.PercpuUsage))\n\t}\n\ttotal := float64(stats.CPUStats.CPUUsage.TotalUsage)\n\tsystem := float64(stats.CPUStats.SystemCPUUsage)\n\n\tcpudiff := total - c.lastCpu\n\tsyscpudiff := system - c.lastSysCpu\n\n\tc.NCpus = ncpus\n\tc.CPUUtil = percent(cpudiff, syscpudiff)\n\tc.lastCpu = total\n\tc.lastSysCpu = system\n\tc.Pids = int(stats.PidsStats.Current)\n}\n\nfunc (c *Docker) ReadMem(stats *api.Stats) {\n\tc.MemUsage = int64(stats.MemoryStats.Usage - stats.MemoryStats.Stats.Cache)\n\tc.MemLimit = int64(stats.MemoryStats.Limit)\n\tc.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))\n}\n\nfunc (c *Docker) ReadNet(stats *api.Stats) {\n\tvar rx, tx int64\n\tfor _, network := range stats.Networks {\n\t\trx += int64(network.RxBytes)\n\t\ttx += int64(network.TxBytes)\n\t}\n\tc.NetRx, c.NetTx = rx, tx\n}\n\nfunc (c *Docker) ReadIO(stats *api.Stats) {\n\tvar read, write int64\n\tfor _, blk := range stats.BlkioStats.IOServiceBytesRecursive {\n\t\tif blk.Op == \"Read\" {\n\t\t\tread += int64(blk.Value)\n\t\t}\n\t\tif blk.Op == \"Write\" {\n\t\t\twrite += int64(blk.Value)\n\t\t}\n\t}\n\tc.IOBytesRead, c.IOBytesWrite = read, write\n}\n"
  },
  {
    "path": "connector/collector/docker_logs.go",
    "content": "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.com/fsouza/go-dockerclient\"\n)\n\ntype DockerLogs struct {\n\tid     string\n\tclient *api.Client\n\tdone   chan bool\n}\n\nfunc NewDockerLogs(id string, client *api.Client) *DockerLogs {\n\treturn &DockerLogs{\n\t\tid:     id,\n\t\tclient: client,\n\t\tdone:   make(chan bool),\n\t}\n}\n\nfunc (l *DockerLogs) Stream() chan models.Log {\n\tr, w := io.Pipe()\n\tlogCh := make(chan models.Log)\n\tctx, cancel := context.WithCancel(context.Background())\n\n\topts := api.LogsOptions{\n\t\tContext:      ctx,\n\t\tContainer:    l.id,\n\t\tOutputStream: w,\n\t\t//ErrorStream:  w,\n\t\tStdout:      true,\n\t\tStderr:      true,\n\t\tTail:        \"20\",\n\t\tFollow:      true,\n\t\tTimestamps:  true,\n\t\tRawTerminal: true,\n\t}\n\n\t// read io pipe into channel\n\tgo func() {\n\t\tscanner := bufio.NewScanner(r)\n\t\tfor scanner.Scan() {\n\t\t\tparts := strings.SplitN(scanner.Text(), \" \", 2)\n\t\t\tif len(parts) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(parts) < 2 {\n\t\t\t\tlogCh <- models.Log{Timestamp: l.parseTime(\"\"), Message: parts[0]}\n\t\t\t} else {\n\t\t\t\tlogCh <- models.Log{Timestamp: l.parseTime(parts[0]), Message: parts[1]}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// connect to container log stream\n\tgo func() {\n\t\terr := l.client.Logs(opts)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"error reading container logs: %s\", err)\n\t\t}\n\t\tlog.Infof(\"log reader stopped for container: %s\", l.id)\n\t}()\n\n\tgo func() {\n\t\t<-l.done\n\t\tcancel()\n\t}()\n\n\tlog.Infof(\"log reader started for container: %s\", l.id)\n\treturn logCh\n}\n\nfunc (l *DockerLogs) Stop() { l.done <- true }\n\nfunc (l *DockerLogs) parseTime(s string) time.Time {\n\tts, err := time.Parse(time.RFC3339Nano, s)\n\tif err == nil {\n\t\treturn ts\n\t}\n\n\tts, err2 := time.Parse(time.RFC3339Nano, l.stripPfx(s))\n\tif err2 == nil {\n\t\treturn ts\n\t}\n\n\tlog.Errorf(\"failed to parse container log: %s\", err)\n\tlog.Errorf(\"failed to parse container log2: %s\", err2)\n\treturn time.Now()\n}\n\n// attempt to strip message header prefix from a given raw docker log string\nfunc (l *DockerLogs) stripPfx(s string) string {\n\tb := []byte(s)\n\tif len(b) > 8 {\n\t\treturn string(b[8:])\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "connector/collector/main.go",
    "content": "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 = logging.Init()\n\ntype LogCollector interface {\n\tStream() chan models.Log\n\tStop()\n}\n\ntype Collector interface {\n\tStream() chan models.Metrics\n\tLogs() LogCollector\n\tRunning() bool\n\tStart()\n\tStop()\n}\n\nfunc round(num float64) int {\n\treturn int(num + math.Copysign(0.5, num))\n}\n\n// return rounded percentage\nfunc percent(val float64, total float64) int {\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\treturn round((val / total) * 100)\n}\n"
  },
  {
    "path": "connector/collector/mock.go",
    "content": "//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/models\"\n)\n\n// Mock collector\ntype Mock struct {\n\tmodels.Metrics\n\tstream     chan models.Metrics\n\tdone       bool\n\trunning    bool\n\taggression int64\n}\n\nfunc NewMock(a int64) *Mock {\n\tc := &Mock{\n\t\tMetrics:    models.Metrics{},\n\t\taggression: a,\n\t}\n\tc.MemLimit = 2147483648\n\treturn c\n}\n\nfunc (c *Mock) Running() bool {\n\treturn c.running\n}\n\nfunc (c *Mock) Start() {\n\tc.done = false\n\tc.stream = make(chan models.Metrics)\n\tgo c.run()\n}\n\nfunc (c *Mock) Stop() {\n\tc.running = false\n\tc.done = true\n}\n\nfunc (c *Mock) Stream() chan models.Metrics {\n\treturn c.stream\n}\n\nfunc (c *Mock) Logs() LogCollector {\n\treturn &MockLogs{make(chan bool)}\n}\n\nfunc (c *Mock) run() {\n\tc.running = true\n\trand.Seed(int64(time.Now().Nanosecond()))\n\tdefer close(c.stream)\n\n\t// set to random static value, once\n\tc.Pids = rand.Intn(12)\n\tc.IOBytesRead = rand.Int63n(8098) * c.aggression\n\tc.IOBytesWrite = rand.Int63n(8098) * c.aggression\n\n\tfor {\n\t\tc.CPUUtil += rand.Intn(2) * int(c.aggression)\n\t\tif c.CPUUtil >= 100 {\n\t\t\tc.CPUUtil = 0\n\t\t}\n\n\t\tc.NetTx += rand.Int63n(60) * c.aggression\n\t\tc.NetRx += rand.Int63n(60) * c.aggression\n\t\tc.MemUsage += rand.Int63n(c.MemLimit/512) * c.aggression\n\t\tif c.MemUsage > c.MemLimit {\n\t\t\tc.MemUsage = 0\n\t\t}\n\t\tc.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))\n\t\tc.stream <- c.Metrics\n\t\tif c.done {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\n\tc.running = false\n}\n"
  },
  {
    "path": "connector/collector/mock_logs.go",
    "content": "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 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.\"\n\ntype MockLogs struct {\n\tdone chan bool\n}\n\nfunc (l *MockLogs) Stream() chan models.Log {\n\tlogCh := make(chan models.Log)\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-l.done:\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tlogCh <- models.Log{Timestamp: time.Now(), Message: mockLog}\n\t\t\t\ttime.Sleep(250 * time.Millisecond)\n\t\t\t}\n\t\t}\n\t}()\n\treturn logCh\n}\n\nfunc (l *MockLogs) Stop() { l.done <- true }\n"
  },
  {
    "path": "connector/collector/proc.go",
    "content": "//go:build linux\n// +build linux\n\npackage collector\n\nimport (\n\tlinuxproc \"github.com/c9s/goprocinfo/linux\"\n)\n\nvar sysMemTotal = getSysMemTotal()\n\nconst (\n\tclockTicksPerSecond  uint64 = 100\n\tnanoSecondsPerSecond        = 1e9\n)\n\nfunc getSysMemTotal() int64 {\n\tstat, err := linuxproc.ReadMemInfo(\"/proc/meminfo\")\n\tif err != nil {\n\t\tlog.Errorf(\"error reading system stats: %s\", err)\n\t\treturn 0\n\t}\n\treturn int64(stat.MemTotal * 1024)\n}\n\n// return cumulative system cpu usage in nanoseconds\nfunc getSysCPUUsage() uint64 {\n\tstat, err := linuxproc.ReadStat(\"/proc/stat\")\n\tif err != nil {\n\t\tlog.Errorf(\"error reading system stats: %s\", err)\n\t\treturn 0\n\t}\n\n\tsum := stat.CPUStatAll.User +\n\t\tstat.CPUStatAll.Nice +\n\t\tstat.CPUStatAll.System +\n\t\tstat.CPUStatAll.Idle +\n\t\tstat.CPUStatAll.IOWait +\n\t\tstat.CPUStatAll.IRQ +\n\t\tstat.CPUStatAll.SoftIRQ +\n\t\tstat.CPUStatAll.Steal +\n\t\tstat.CPUStatAll.Guest +\n\t\tstat.CPUStatAll.GuestNice\n\n\treturn (sum * nanoSecondsPerSecond) / clockTicksPerSecond\n}\n"
  },
  {
    "path": "connector/collector/runc.go",
    "content": "//go:build linux\n// +build linux\n\npackage collector\n\nimport (\n\t\"time\"\n\n\t\"github.com/opencontainers/runc/libcontainer\"\n\t\"github.com/opencontainers/runc/libcontainer/cgroups\"\n\t\"github.com/opencontainers/runc/types\"\n\n\t\"github.com/bcicen/ctop/models\"\n)\n\n// Runc collector\ntype Runc struct {\n\tmodels.Metrics\n\tid         string\n\tlibc       libcontainer.Container\n\tstream     chan models.Metrics\n\tdone       bool\n\trunning    bool\n\tinterval   int // collection interval, in seconds\n\tlastCpu    float64\n\tlastSysCpu float64\n}\n\nfunc NewRunc(libc libcontainer.Container) *Runc {\n\tc := &Runc{\n\t\tMetrics:  models.Metrics{},\n\t\tid:       libc.ID(),\n\t\tlibc:     libc,\n\t\tinterval: 1,\n\t}\n\treturn c\n}\n\nfunc (c *Runc) Running() bool {\n\treturn c.running\n}\n\nfunc (c *Runc) Start() {\n\tc.done = false\n\tc.stream = make(chan models.Metrics)\n\tgo c.run()\n}\n\nfunc (c *Runc) Stop() {\n\tc.running = false\n\tc.done = true\n}\n\nfunc (c *Runc) Stream() chan models.Metrics {\n\treturn c.stream\n}\n\nfunc (c *Runc) Logs() LogCollector {\n\treturn nil\n}\n\nfunc (c *Runc) run() {\n\tc.running = true\n\tdefer close(c.stream)\n\tlog.Debugf(\"collector started for container: %s\", c.id)\n\n\tfor {\n\t\tstats, err := c.libc.Stats()\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to collect stats for container %s:\\n%s\", c.id, err)\n\t\t\tbreak\n\t\t}\n\n\t\tc.ReadCPU(stats.CgroupStats)\n\t\tc.ReadMem(stats.CgroupStats)\n\t\tc.ReadNet(stats.Interfaces)\n\n\t\tc.stream <- c.Metrics\n\t\tif c.done {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\n\tc.running = false\n}\n\nfunc (c *Runc) ReadCPU(stats *cgroups.Stats) {\n\tu := stats.CpuStats.CpuUsage\n\tncpus := uint8(len(u.PercpuUsage))\n\ttotal := float64(u.TotalUsage)\n\tsystem := float64(getSysCPUUsage())\n\n\tcpudiff := total - c.lastCpu\n\tsyscpudiff := system - c.lastSysCpu\n\n\tc.NCpus = ncpus\n\tc.CPUUtil = percent(cpudiff, syscpudiff)\n\tc.lastCpu = total\n\tc.lastSysCpu = system\n\tc.Pids = int(stats.PidsStats.Current)\n}\n\nfunc (c *Runc) ReadMem(stats *cgroups.Stats) {\n\tc.MemUsage = int64(stats.MemoryStats.Usage.Usage)\n\tc.MemLimit = int64(stats.MemoryStats.Usage.Limit)\n\tif c.MemLimit > sysMemTotal && sysMemTotal > 0 {\n\t\tc.MemLimit = sysMemTotal\n\t}\n\tc.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))\n}\n\nfunc (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {\n\tvar rx, tx int64\n\tfor _, network := range interfaces {\n\t\trx += int64(network.RxBytes)\n\t\ttx += int64(network.TxBytes)\n\t}\n\tc.NetRx, c.NetTx = rx, tx\n}\n\nfunc (c *Runc) ReadIO(stats *cgroups.Stats) {\n\tvar read, write int64\n\tfor _, blk := range stats.BlkioStats.IoServiceBytesRecursive {\n\t\tif blk.Op == \"Read\" {\n\t\t\tread = int64(blk.Value)\n\t\t}\n\t\tif blk.Op == \"Write\" {\n\t\t\twrite = int64(blk.Value)\n\t\t}\n\t}\n\tc.IOBytesRead, c.IOBytesWrite = read, write\n}\n"
  },
  {
    "path": "connector/docker.go",
    "content": "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\"github.com/bcicen/ctop/connector/collector\"\n\t\"github.com/bcicen/ctop/connector/manager\"\n\t\"github.com/bcicen/ctop/container\"\n\tapi \"github.com/fsouza/go-dockerclient\"\n)\n\nfunc init() { enabled[\"docker\"] = NewDocker }\n\nvar actionToStatus = map[string]string{\n\t\"start\":   \"running\",\n\t\"die\":     \"exited\",\n\t\"stop\":    \"exited\",\n\t\"pause\":   \"paused\",\n\t\"unpause\": \"running\",\n}\n\ntype StatusUpdate struct {\n\tCid    string\n\tField  string // \"status\" or \"health\"\n\tStatus string\n}\n\ntype Docker struct {\n\tclient       *api.Client\n\tcontainers   map[string]*container.Container\n\tneedsRefresh chan string // container IDs requiring refresh\n\tstatuses     chan StatusUpdate\n\tclosed       chan struct{}\n\tlock         sync.RWMutex\n}\n\nfunc NewDocker() (Connector, error) {\n\t// init docker client\n\tclient, err := api.NewClientFromEnv()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcm := &Docker{\n\t\tclient:       client,\n\t\tcontainers:   make(map[string]*container.Container),\n\t\tneedsRefresh: make(chan string, 60),\n\t\tstatuses:     make(chan StatusUpdate, 60),\n\t\tclosed:       make(chan struct{}),\n\t\tlock:         sync.RWMutex{},\n\t}\n\n\t// query info as pre-flight healthcheck\n\tinfo, err := client.Info()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlog.Debugf(\"docker-connector ID: %s\", info.ID)\n\tlog.Debugf(\"docker-connector Driver: %s\", info.Driver)\n\tlog.Debugf(\"docker-connector Images: %d\", info.Images)\n\tlog.Debugf(\"docker-connector Name: %s\", info.Name)\n\tlog.Debugf(\"docker-connector ServerVersion: %s\", info.ServerVersion)\n\n\tgo cm.Loop()\n\tgo cm.LoopStatuses()\n\tcm.refreshAll()\n\tgo cm.watchEvents()\n\treturn cm, nil\n}\n\n// Docker implements Connector\nfunc (cm *Docker) Wait() struct{} { return <-cm.closed }\n\n// Docker events watcher\nfunc (cm *Docker) watchEvents() {\n\tlog.Info(\"docker event listener starting\")\n\tevents := make(chan *api.APIEvents)\n\topts := api.EventsOptions{Filters: map[string][]string{\n\t\t\"type\":  {\"container\"},\n\t\t\"event\": {\"create\", \"start\", \"health_status\", \"pause\", \"unpause\", \"stop\", \"die\", \"destroy\"},\n\t},\n\t}\n\tcm.client.AddEventListenerWithOptions(opts, events)\n\n\tfor e := range events {\n\t\tactionName := e.Action\n\t\tswitch actionName {\n\t\t// most frequent event is a health checks\n\t\tcase \"health_status: healthy\", \"health_status: unhealthy\":\n\t\t\tsepIdx := strings.Index(actionName, \": \")\n\t\t\thealthStatus := e.Action[sepIdx+2:]\n\t\t\tif log.IsEnabledFor(logging.DEBUG) {\n\t\t\t\tlog.Debugf(\"handling docker event: action=health_status id=%s %s\", e.ID, healthStatus)\n\t\t\t}\n\t\t\tcm.statuses <- StatusUpdate{e.ID, \"health\", healthStatus}\n\t\tcase \"create\":\n\t\t\tif log.IsEnabledFor(logging.DEBUG) {\n\t\t\t\tlog.Debugf(\"handling docker event: action=create id=%s\", e.ID)\n\t\t\t}\n\t\t\tcm.needsRefresh <- e.ID\n\t\tcase \"destroy\":\n\t\t\tif log.IsEnabledFor(logging.DEBUG) {\n\t\t\t\tlog.Debugf(\"handling docker event: action=destroy id=%s\", e.ID)\n\t\t\t}\n\t\t\tcm.delByID(e.ID)\n\t\tdefault:\n\t\t\t// check if this action changes status e.g. start -> running\n\t\t\tstatus := actionToStatus[actionName]\n\t\t\tif status != \"\" {\n\t\t\t\tif log.IsEnabledFor(logging.DEBUG) {\n\t\t\t\t\tlog.Debugf(\"handling docker event: action=%s id=%s %s\", actionName, e.ID, status)\n\t\t\t\t}\n\t\t\t\tcm.statuses <- StatusUpdate{e.ID, \"status\", status}\n\t\t\t}\n\t\t}\n\t}\n\tlog.Info(\"docker event listener exited\")\n\tclose(cm.closed)\n}\n\nfunc portsFormat(ports map[api.Port][]api.PortBinding) string {\n\tvar exposed []string\n\tvar published []string\n\n\tfor k, v := range ports {\n\t\tif len(v) == 0 {\n\t\t\texposed = append(exposed, string(k))\n\t\t\tcontinue\n\t\t}\n\t\tfor _, binding := range v {\n\t\t\ts := fmt.Sprintf(\"%s:%s -> %s\", binding.HostIP, binding.HostPort, k)\n\t\t\tpublished = append(published, s)\n\t\t}\n\t}\n\n\treturn strings.Join(append(exposed, published...), \"\\n\")\n}\n\nfunc webPort(ports map[api.Port][]api.PortBinding) string {\n\tfor _, v := range ports {\n\t\tif len(v) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, binding := range v {\n\t\t\tpublishedIp := binding.HostIP\n\t\t\tif publishedIp == \"0.0.0.0\" {\n\t\t\t\tpublishedIp = \"localhost\"\n\t\t\t}\n\t\t\tpublishedWebPort := fmt.Sprintf(\"%s:%s\", publishedIp, binding.HostPort)\n\t\t\treturn publishedWebPort\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc ipsFormat(networks map[string]api.ContainerNetwork) string {\n\tvar ips []string\n\n\tfor k, v := range networks {\n\t\ts := fmt.Sprintf(\"%s:%s\", k, v.IPAddress)\n\t\tips = append(ips, s)\n\t}\n\n\treturn strings.Join(ips, \"\\n\")\n}\n\nfunc (cm *Docker) refresh(c *container.Container) {\n\tinsp, found, failed := cm.inspect(c.Id)\n\tif failed {\n\t\treturn\n\t}\n\t// remove container if no longer exists\n\tif !found {\n\t\tcm.delByID(c.Id)\n\t\treturn\n\t}\n\tc.SetMeta(\"name\", shortName(insp.Name))\n\tc.SetMeta(\"image\", insp.Config.Image)\n\tc.SetMeta(\"IPs\", ipsFormat(insp.NetworkSettings.Networks))\n\tc.SetMeta(\"ports\", portsFormat(insp.NetworkSettings.Ports))\n\twebPort := webPort(insp.NetworkSettings.Ports)\n\tif webPort != \"\" {\n\t\tc.SetMeta(\"Web Port\", webPort)\n\t}\n\tc.SetMeta(\"created\", insp.Created.Format(\"Mon Jan 02 15:04:05 2006\"))\n\tc.SetMeta(\"uptime\", calcUptime(insp))\n\tc.SetMeta(\"health\", insp.State.Health.Status)\n\tc.SetMeta(\"[ENV-VAR]\", strings.Join(insp.Config.Env, \";\"))\n\tc.SetState(insp.State.Status)\n}\n\nfunc (cm *Docker) inspect(id string) (insp *api.Container, found bool, failed bool) {\n\tc, err := cm.client.InspectContainer(id)\n\tif err != nil {\n\t\tif _, notFound := err.(*api.NoSuchContainer); notFound {\n\t\t\treturn c, false, false\n\t\t}\n\t\t// other error e.g. connection failed\n\t\tlog.Errorf(\"%s (%T)\", err.Error(), err)\n\t\treturn c, false, true\n\t}\n\treturn c, true, false\n}\n\nfunc calcUptime(insp *api.Container) string {\n\tendTime := insp.State.FinishedAt\n\tif endTime.IsZero() || insp.State.Running {\n\t\tendTime = time.Now()\n\t}\n\tuptime := endTime.Sub(insp.State.StartedAt)\n\treturn durafmt.Parse(uptime).LimitFirstN(1).String()\n}\n\n// Mark all container IDs for refresh\nfunc (cm *Docker) refreshAll() {\n\topts := api.ListContainersOptions{All: true}\n\tallContainers, err := cm.client.ListContainers(opts)\n\tif err != nil {\n\t\tlog.Errorf(\"%s (%T)\", err.Error(), err)\n\t\treturn\n\t}\n\n\tfor _, i := range allContainers {\n\t\tc := cm.MustGet(i.ID)\n\t\tc.SetMeta(\"name\", shortName(i.Names[0]))\n\t\tc.SetState(i.State)\n\t\tcm.needsRefresh <- c.Id\n\t}\n}\n\nfunc (cm *Docker) Loop() {\n\tfor {\n\t\tselect {\n\t\tcase id := <-cm.needsRefresh:\n\t\t\tc := cm.MustGet(id)\n\t\t\tcm.refresh(c)\n\t\tcase <-cm.closed:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (cm *Docker) LoopStatuses() {\n\tfor {\n\t\tselect {\n\t\tcase statusUpdate := <-cm.statuses:\n\t\t\tc, _ := cm.Get(statusUpdate.Cid)\n\t\t\tif c != nil {\n\t\t\t\tif statusUpdate.Field == \"health\" {\n\t\t\t\t\tc.SetMeta(\"health\", statusUpdate.Status)\n\t\t\t\t} else {\n\t\t\t\t\tc.SetState(statusUpdate.Status)\n\t\t\t\t}\n\t\t\t}\n\t\tcase <-cm.closed:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// MustGet gets a single container, creating one anew if not existing\nfunc (cm *Docker) MustGet(id string) *container.Container {\n\tc, ok := cm.Get(id)\n\t// append container struct for new containers\n\tif !ok {\n\t\t// create collector\n\t\tcollector := collector.NewDocker(cm.client, id)\n\t\t// create manager\n\t\tmanager := manager.NewDocker(cm.client, id)\n\t\t// create container\n\t\tc = container.New(id, collector, manager)\n\t\tcm.lock.Lock()\n\t\tcm.containers[id] = c\n\t\tcm.lock.Unlock()\n\t}\n\treturn c\n}\n\n// Docker implements Connector\nfunc (cm *Docker) Get(id string) (*container.Container, bool) {\n\tcm.lock.Lock()\n\tc, ok := cm.containers[id]\n\tcm.lock.Unlock()\n\treturn c, ok\n}\n\n// Remove containers by ID\nfunc (cm *Docker) delByID(id string) {\n\tcm.lock.Lock()\n\tdelete(cm.containers, id)\n\tcm.lock.Unlock()\n\tlog.Infof(\"removed dead container: %s\", id)\n}\n\n// Docker implements Connector\nfunc (cm *Docker) All() (containers container.Containers) {\n\tcm.lock.Lock()\n\tfor _, c := range cm.containers {\n\t\tcontainers = append(containers, c)\n\t}\n\n\tcontainers.Sort()\n\tcontainers.Filter()\n\tcm.lock.Unlock()\n\treturn containers\n}\n\n// use primary container name\nfunc shortName(name string) string {\n\treturn strings.TrimPrefix(name, \"/\")\n}\n"
  },
  {
    "path": "connector/main.go",
    "content": "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/logging\"\n)\n\nvar (\n\tlog     = logging.Init()\n\tenabled = make(map[string]ConnectorFn)\n)\n\ntype ConnectorFn func() (Connector, error)\n\ntype Connector interface {\n\t// All returns a pre-sorted container.Containers of all discovered containers\n\tAll() container.Containers\n\t// Get returns a single container.Container by ID\n\tGet(string) (*container.Container, bool)\n\t// Wait blocks until the underlying connection is lost\n\tWait() struct{}\n}\n\n// ConnectorSuper provides initial connection and retry on failure for\n// an undlerying Connector type\ntype ConnectorSuper struct {\n\tconn   Connector\n\tconnFn ConnectorFn\n\terr    error\n\tlock   sync.RWMutex\n}\n\nfunc NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {\n\tcs := &ConnectorSuper{\n\t\tconnFn: connFn,\n\t\terr:    fmt.Errorf(\"connecting...\"),\n\t}\n\tgo cs.loop()\n\treturn cs\n}\n\n// Get returns the underlying Connector, or nil and an error\n// if the Connector is not yet initialized or is disconnected.\nfunc (cs *ConnectorSuper) Get() (Connector, error) {\n\tcs.lock.RLock()\n\tdefer cs.lock.RUnlock()\n\tif cs.err != nil {\n\t\treturn nil, cs.err\n\t}\n\treturn cs.conn, nil\n}\n\nfunc (cs *ConnectorSuper) setError(err error) {\n\tcs.lock.Lock()\n\tdefer cs.lock.Unlock()\n\tcs.err = err\n}\n\nfunc (cs *ConnectorSuper) loop() {\n\tconst interval = 3\n\tfor {\n\t\tlog.Infof(\"initializing connector\")\n\n\t\tconn, err := cs.connFn()\n\t\tif err != nil {\n\t\t\tcs.setError(err)\n\t\t\tlog.Errorf(\"failed to initialize connector: %s (%T)\", err, err)\n\t\t\tlog.Errorf(\"retrying in %ds\", interval)\n\t\t\ttime.Sleep(interval * time.Second)\n\t\t} else {\n\t\t\tcs.conn = conn\n\t\t\tcs.setError(nil)\n\t\t\tlog.Infof(\"successfully initialized connector\")\n\n\t\t\t// wait until connection closed\n\t\t\tcs.conn.Wait()\n\t\t\tcs.setError(fmt.Errorf(\"attempting to reconnect...\"))\n\t\t\tlog.Infof(\"connector closed\")\n\t\t}\n\t}\n}\n\n// Enabled returns names for all enabled connectors on the current platform\nfunc Enabled() (a []string) {\n\tfor k, _ := range enabled {\n\t\ta = append(a, k)\n\t}\n\tsort.Strings(a)\n\treturn a\n}\n\n// ByName returns a ConnectorSuper for a given name, or error if the connector\n// does not exists on the current platform\nfunc ByName(s string) (*ConnectorSuper, error) {\n\tif cfn, ok := enabled[s]; ok {\n\t\treturn NewConnectorSuper(cfn), nil\n\t}\n\treturn nil, fmt.Errorf(\"invalid connector type \\\"%s\\\"\", s)\n}\n"
  },
  {
    "path": "connector/manager/docker.go",
    "content": "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 Docker struct {\n\tid     string\n\tclient *api.Client\n}\n\nfunc NewDocker(client *api.Client, id string) *Docker {\n\treturn &Docker{\n\t\tid:     id,\n\t\tclient: client,\n\t}\n}\n\n// Do not allow to close reader (i.e. /dev/stdin which docker client tries to close after command execution)\ntype noClosableReader struct {\n\tio.Reader\n}\n\nfunc (w *noClosableReader) Read(p []byte) (n int, err error) {\n\treturn w.Reader.Read(p)\n}\n\nconst (\n\tSTDIN  = 0\n\tSTDOUT = 1\n\tSTDERR = 2\n)\n\nvar wrongFrameFormat = errors.New(\"Wrong frame format\")\n\n// A frame has a Header and a Payload\n// Header: [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}\n// STREAM_TYPE can be:\n//    0: stdin (is written on stdout)\n//    1: stdout\n//    2: stderr\n// SIZE1, SIZE2, SIZE3, SIZE4 are the four bytes of the uint32 size encoded as big endian.\n// But we don't use size, because we don't need to find the end of frame.\ntype frameWriter struct {\n\tstdout io.Writer\n\tstderr io.Writer\n\tstdin  io.Writer\n}\n\nfunc (w *frameWriter) Write(p []byte) (n int, err error) {\n\t// drop initial empty frames\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\n\tif len(p) > 8 {\n\t\tvar targetWriter io.Writer\n\t\tswitch p[0] {\n\t\tcase STDIN:\n\t\t\ttargetWriter = w.stdin\n\t\t\tbreak\n\t\tcase STDOUT:\n\t\t\ttargetWriter = w.stdout\n\t\t\tbreak\n\t\tcase STDERR:\n\t\t\ttargetWriter = w.stderr\n\t\t\tbreak\n\t\tdefault:\n\t\t\treturn 0, wrongFrameFormat\n\t\t}\n\n\t\tn, err := targetWriter.Write(p[8:])\n\t\treturn n + 8, err\n\t}\n\n\treturn 0, wrongFrameFormat\n}\n\nfunc (dc *Docker) Exec(cmd []string) error {\n\texecCmd, err := dc.client.CreateExec(api.CreateExecOptions{\n\t\tAttachStdin:  true,\n\t\tAttachStdout: true,\n\t\tAttachStderr: true,\n\t\tCmd:          cmd,\n\t\tContainer:    dc.id,\n\t\tTty:          true,\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn dc.client.StartExec(execCmd.ID, api.StartExecOptions{\n\t\tInputStream:  &noClosableReader{os.Stdin},\n\t\tOutputStream: &frameWriter{os.Stdout, os.Stderr, os.Stdin},\n\t\tErrorStream:  os.Stderr,\n\t\tRawTerminal:  true,\n\t})\n}\n\nfunc (dc *Docker) Start() error {\n\tc, err := dc.client.InspectContainer(dc.id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot inspect container: %v\", err)\n\t}\n\n\tif err := dc.client.StartContainer(c.ID, c.HostConfig); err != nil {\n\t\treturn fmt.Errorf(\"cannot start container: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (dc *Docker) Stop() error {\n\tif err := dc.client.StopContainer(dc.id, 3); err != nil {\n\t\treturn fmt.Errorf(\"cannot stop container: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (dc *Docker) Remove() error {\n\tif err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {\n\t\treturn fmt.Errorf(\"cannot remove container: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (dc *Docker) Pause() error {\n\tif err := dc.client.PauseContainer(dc.id); err != nil {\n\t\treturn fmt.Errorf(\"cannot pause container: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (dc *Docker) Unpause() error {\n\tif err := dc.client.UnpauseContainer(dc.id); err != nil {\n\t\treturn fmt.Errorf(\"cannot unpause container: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (dc *Docker) Restart() error {\n\tif err := dc.client.RestartContainer(dc.id, 3); err != nil {\n\t\treturn fmt.Errorf(\"cannot restart container: %v\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "connector/manager/main.go",
    "content": "package manager\n\nimport \"errors\"\n\nvar ActionNotImplErr = errors.New(\"action not implemented\")\n\ntype Manager interface {\n\tStart() error\n\tStop() error\n\tRemove() error\n\tPause() error\n\tUnpause() error\n\tRestart() error\n\tExec(cmd []string) error\n}\n"
  },
  {
    "path": "connector/manager/mock.go",
    "content": "package manager\n\ntype Mock struct{}\n\nfunc NewMock() *Mock {\n\treturn &Mock{}\n}\n\nfunc (m *Mock) Start() error {\n\treturn ActionNotImplErr\n}\n\nfunc (m *Mock) Stop() error {\n\treturn ActionNotImplErr\n}\n\nfunc (m *Mock) Remove() error {\n\treturn ActionNotImplErr\n}\n\nfunc (m *Mock) Pause() error {\n\treturn ActionNotImplErr\n}\n\nfunc (m *Mock) Unpause() error {\n\treturn ActionNotImplErr\n}\n\nfunc (m *Mock) Restart() error {\n\treturn ActionNotImplErr\n}\n\nfunc (m *Mock) Exec(cmd []string) error {\n\treturn ActionNotImplErr\n}\n"
  },
  {
    "path": "connector/manager/runc.go",
    "content": "package manager\n\ntype Runc struct{}\n\nfunc NewRunc() *Runc {\n\treturn &Runc{}\n}\n\nfunc (rc *Runc) Start() error {\n\treturn ActionNotImplErr\n}\n\nfunc (rc *Runc) Stop() error {\n\treturn ActionNotImplErr\n}\n\nfunc (rc *Runc) Remove() error {\n\treturn ActionNotImplErr\n}\n\nfunc (rc *Runc) Pause() error {\n\treturn ActionNotImplErr\n}\n\nfunc (rc *Runc) Unpause() error {\n\treturn ActionNotImplErr\n}\n\nfunc (rc *Runc) Restart() error {\n\treturn ActionNotImplErr\n}\n\nfunc (rc *Runc) Exec(cmd []string) error {\n\treturn ActionNotImplErr\n}\n"
  },
  {
    "path": "connector/mock.go",
    "content": "//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/ctop/connector/collector\"\n\t\"github.com/bcicen/ctop/connector/manager\"\n\t\"github.com/bcicen/ctop/container\"\n\t\"github.com/jgautheron/codename-generator\"\n\t\"github.com/nu7hatch/gouuid\"\n)\n\nfunc init() { enabled[\"mock\"] = NewMock }\n\ntype Mock struct {\n\tcontainers container.Containers\n}\n\nfunc NewMock() (Connector, error) {\n\tcs := &Mock{}\n\tgo cs.Init()\n\tgo cs.Loop()\n\treturn cs, nil\n}\n\n// Create Mock containers\nfunc (cs *Mock) Init() {\n\trand.Seed(int64(time.Now().Nanosecond()))\n\n\tfor i := 0; i < 4; i++ {\n\t\tcs.makeContainer(3, true)\n\t}\n\n\tfor i := 0; i < 16; i++ {\n\t\tcs.makeContainer(1, false)\n\t}\n\n}\n\nfunc (cs *Mock) Wait() struct{} {\n\tch := make(chan struct{})\n\tgo func() {\n\t\ttime.Sleep(30 * time.Second)\n\t\tclose(ch)\n\t}()\n\treturn <-ch\n}\n\nvar healthStates = []string{\"starting\", \"healthy\", \"unhealthy\"}\n\nfunc (cs *Mock) makeContainer(aggression int64, health bool) {\n\tcollector := collector.NewMock(aggression)\n\tmanager := manager.NewMock()\n\tc := container.New(makeID(), collector, manager)\n\tc.SetMeta(\"name\", makeName())\n\tc.SetState(makeState())\n\tif health {\n\t\tvar i int\n\t\tc.SetMeta(\"health\", healthStates[i])\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\ti++\n\t\t\t\tif i >= len(healthStates) {\n\t\t\t\t\ti = 0\n\t\t\t\t}\n\t\t\t\tc.SetMeta(\"health\", healthStates[i])\n\t\t\t\ttime.Sleep(12 * time.Second)\n\t\t\t}\n\t\t}()\n\t}\n\tcs.containers = append(cs.containers, c)\n}\n\nfunc (cs *Mock) Loop() {\n\titer := 0\n\tfor {\n\t\t// Change state for random container\n\t\tif iter%5 == 0 && len(cs.containers) > 0 {\n\t\t\trandC := cs.containers[rand.Intn(len(cs.containers))]\n\t\t\trandC.SetState(makeState())\n\t\t}\n\t\titer++\n\t\ttime.Sleep(3 * time.Second)\n\t}\n}\n\n// Get a single container, by ID\nfunc (cs *Mock) Get(id string) (*container.Container, bool) {\n\tfor _, c := range cs.containers {\n\t\tif c.Id == id {\n\t\t\treturn c, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// All returns array of all containers, sorted by field\nfunc (cs *Mock) All() container.Containers {\n\tcs.containers.Sort()\n\tcs.containers.Filter()\n\treturn cs.containers\n}\n\n// Remove containers by ID\nfunc (cs *Mock) delByID(id string) {\n\tfor n, c := range cs.containers {\n\t\tif c.Id == id {\n\t\t\tcs.del(n)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Remove one or more containers by index\nfunc (cs *Mock) del(idx ...int) {\n\tfor _, i := range idx {\n\t\tcs.containers = append(cs.containers[:i], cs.containers[i+1:]...)\n\t}\n\tlog.Infof(\"removed %d dead containers\", len(idx))\n}\n\nfunc makeID() string {\n\tu, err := uuid.NewV4()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn strings.Replace(u.String(), \"-\", \"\", -1)[:12]\n}\n\nfunc makeName() string {\n\tn, err := codename.Get(codename.Sanitized)\n\tnsp := strings.Split(n, \"-\")\n\tif len(nsp) > 2 {\n\t\tn = strings.Join(nsp[:2], \"-\")\n\t}\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn strings.Replace(n, \"-\", \"_\", -1)\n}\n\nfunc makeState() string {\n\tswitch rand.Intn(10) {\n\tcase 0, 1, 2:\n\t\treturn \"exited\"\n\tcase 3:\n\t\treturn \"paused\"\n\t}\n\treturn \"running\"\n}\n"
  },
  {
    "path": "connector/runc.go",
    "content": "//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\"time\"\n\n\t\"github.com/bcicen/ctop/connector/collector\"\n\t\"github.com/bcicen/ctop/connector/manager\"\n\t\"github.com/bcicen/ctop/container\"\n\t\"github.com/opencontainers/runc/libcontainer\"\n)\n\nfunc init() { enabled[\"runc\"] = NewRunc }\n\ntype RuncOpts struct {\n\troot           string // runc root path\n\tsystemdCgroups bool   // use systemd cgroups\n}\n\nfunc NewRuncOpts() (RuncOpts, error) {\n\tvar opts RuncOpts\n\t// read runc root path\n\troot := os.Getenv(\"RUNC_ROOT\")\n\tif root == \"\" {\n\t\troot = \"/run/runc\"\n\t}\n\tabs, err := filepath.Abs(root)\n\tif err != nil {\n\t\treturn opts, err\n\t}\n\topts.root = abs\n\n\t// ensure runc root path is readable\n\t_, err = ioutil.ReadDir(opts.root)\n\tif err != nil {\n\t\treturn opts, err\n\t}\n\n\tif os.Getenv(\"RUNC_SYSTEMD_CGROUP\") == \"1\" {\n\t\topts.systemdCgroups = true\n\t}\n\treturn opts, nil\n}\n\ntype Runc struct {\n\topts          RuncOpts\n\tfactory       libcontainer.Factory\n\tcontainers    map[string]*container.Container\n\tlibContainers map[string]libcontainer.Container\n\tclosed        chan struct{}\n\tneedsRefresh  chan string // container IDs requiring refresh\n\tlock          sync.RWMutex\n}\n\nfunc NewRunc() (Connector, error) {\n\topts, err := NewRuncOpts()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfactory, err := libcontainer.New(opts.root)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcm := &Runc{\n\t\topts:          opts,\n\t\tfactory:       factory,\n\t\tcontainers:    make(map[string]*container.Container),\n\t\tlibContainers: make(map[string]libcontainer.Container),\n\t\tclosed:        make(chan struct{}),\n\t\tlock:          sync.RWMutex{},\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-cm.closed:\n\t\t\t\treturn\n\t\t\tcase <-time.After(5 * time.Second):\n\t\t\t\tcm.refreshAll()\n\t\t\t}\n\t\t}\n\t}()\n\tgo cm.Loop()\n\n\treturn cm, nil\n}\n\nfunc (cm *Runc) GetLibc(id string) libcontainer.Container {\n\t// return previously loaded container\n\tlibc, ok := cm.libContainers[id]\n\tif ok {\n\t\treturn libc\n\t}\n\t// load container\n\tlibc, err := cm.factory.Load(id)\n\tif err != nil {\n\t\t// remove container if no longer exists\n\t\tif errors.Is(err, libcontainer.ErrNotExist) {\n\t\t\tcm.delByID(id)\n\t\t} else {\n\t\t\tlog.Warningf(\"failed to read container: %s\\n\", err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn libc\n}\n\n// update a ctop container from libcontainer\nfunc (cm *Runc) refresh(id string) {\n\tlibc := cm.GetLibc(id)\n\tif libc == nil {\n\t\treturn\n\t}\n\tc := cm.MustGet(id)\n\n\t// remove container if entered destroyed state on last refresh\n\t// this gives adequate time for the collector to be shut down\n\tif c.GetMeta(\"state\") == \"destroyed\" {\n\t\tcm.delByID(id)\n\t\treturn\n\t}\n\n\tstatus, err := libc.Status()\n\tif err != nil {\n\t\tlog.Warningf(\"failed to read status for container: %s\\n\", err)\n\t} else {\n\t\tc.SetState(status.String())\n\t}\n\n\tstate, err := libc.State()\n\tif err != nil {\n\t\tlog.Warningf(\"failed to read state for container: %s\\n\", err)\n\t} else {\n\t\tc.SetMeta(\"created\", state.BaseState.Created.Format(\"Mon Jan 2 15:04:05 2006\"))\n\t}\n\n\tconf := libc.Config()\n\tc.SetMeta(\"rootfs\", conf.Rootfs)\n}\n\n// Read runc root, creating any new containers\nfunc (cm *Runc) refreshAll() {\n\tlist, err := ioutil.ReadDir(cm.opts.root)\n\tif err != nil {\n\t\tlog.Errorf(\"%s (%T)\", err.Error(), err)\n\t\tclose(cm.closed)\n\t\treturn\n\t}\n\n\tfor _, i := range list {\n\t\tif i.IsDir() {\n\t\t\tname := i.Name()\n\t\t\t// attempt to load\n\t\t\tlibc := cm.GetLibc(name)\n\t\t\tif libc == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_ = cm.MustGet(i.Name()) // ensure container exists\n\t\t}\n\t}\n\n\t// queue all existing containers for refresh\n\tfor id := range cm.containers {\n\t\tcm.needsRefresh <- id\n\t}\n\tlog.Debugf(\"queued %d containers for refresh\", len(cm.containers))\n}\n\nfunc (cm *Runc) Loop() {\n\tfor id := range cm.needsRefresh {\n\t\tcm.refresh(id)\n\t}\n}\n\n// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing\nfunc (cm *Runc) MustGet(id string) *container.Container {\n\tc, ok := cm.Get(id)\n\tif !ok {\n\t\tlibc := cm.GetLibc(id)\n\n\t\t// create collector\n\t\tcollector := collector.NewRunc(libc)\n\n\t\t// create container\n\t\tmanager := manager.NewRunc()\n\t\tc = container.New(id, collector, manager)\n\n\t\tname := libc.ID()\n\t\t// set initial metadata\n\t\tif len(name) > 12 {\n\t\t\tname = name[0:12]\n\t\t}\n\t\tc.SetMeta(\"name\", name)\n\n\t\t// add to map\n\t\tcm.lock.Lock()\n\t\tcm.containers[id] = c\n\t\tcm.libContainers[id] = libc\n\t\tcm.lock.Unlock()\n\t\tlog.Debugf(\"saw new container: %s\", id)\n\t}\n\n\treturn c\n}\n\n// Remove containers by ID\nfunc (cm *Runc) delByID(id string) {\n\tcm.lock.Lock()\n\tdelete(cm.containers, id)\n\tdelete(cm.libContainers, id)\n\tcm.lock.Unlock()\n\tlog.Infof(\"removed dead container: %s\", id)\n}\n\n// Runc implements Connector\nfunc (cm *Runc) Wait() struct{} { return <-cm.closed }\n\n// Runc implements Connector\nfunc (cm *Runc) Get(id string) (*container.Container, bool) {\n\tcm.lock.Lock()\n\tdefer cm.lock.Unlock()\n\tc, ok := cm.containers[id]\n\treturn c, ok\n}\n\n// Runc implements Connector\nfunc (cm *Runc) All() (containers container.Containers) {\n\tcm.lock.Lock()\n\tfor _, c := range cm.containers {\n\t\tcontainers = append(containers, c)\n\t}\n\tcontainers.Sort()\n\tcontainers.Filter()\n\tcm.lock.Unlock()\n\treturn containers\n}\n"
  },
  {
    "path": "container/main.go",
    "content": "package container\n\nimport (\n\t\"github.com/bcicen/ctop/connector/collector\"\n\t\"github.com/bcicen/ctop/connector/manager\"\n\t\"github.com/bcicen/ctop/cwidgets\"\n\t\"github.com/bcicen/ctop/cwidgets/compact\"\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n)\n\nvar (\n\tlog = logging.Init()\n)\n\nconst (\n\trunning = \"running\"\n)\n\n// Metrics and metadata representing a container\ntype Container struct {\n\tmodels.Metrics\n\tId        string\n\tMeta      models.Meta\n\tWidgets   *compact.CompactRow\n\tDisplay   bool // display this container in compact view\n\tupdater   cwidgets.WidgetUpdater\n\tcollector collector.Collector\n\tmanager   manager.Manager\n}\n\nfunc New(id string, collector collector.Collector, manager manager.Manager) *Container {\n\twidgets := compact.NewCompactRow()\n\tshortID := id\n\tif len(shortID) > 12 {\n\t\tshortID = shortID[0:12]\n\t}\n\treturn &Container{\n\t\tMetrics:   models.NewMetrics(),\n\t\tId:        id,\n\t\tMeta:      models.NewMeta(\"id\", shortID),\n\t\tWidgets:   widgets,\n\t\tupdater:   widgets,\n\t\tcollector: collector,\n\t\tmanager:   manager,\n\t}\n}\n\nfunc (c *Container) RecreateWidgets() {\n\tc.SetUpdater(cwidgets.NullWidgetUpdater{})\n\tc.Widgets = compact.NewCompactRow()\n\tc.SetUpdater(c.Widgets)\n}\n\nfunc (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {\n\tc.updater = u\n\tc.updater.SetMeta(c.Meta)\n}\n\nfunc (c *Container) SetMeta(k, v string) {\n\tc.Meta[k] = v\n\tc.updater.SetMeta(c.Meta)\n}\n\nfunc (c *Container) GetMeta(k string) string {\n\treturn c.Meta.Get(k)\n}\n\nfunc (c *Container) SetState(s string) {\n\tc.SetMeta(\"state\", s)\n\t// start collector, if needed\n\tif s == running && !c.collector.Running() {\n\t\tc.collector.Start()\n\t\tc.Read(c.collector.Stream())\n\t}\n\t// stop collector, if needed\n\tif s != running && c.collector.Running() {\n\t\tc.collector.Stop()\n\t}\n}\n\n// Logs returns container log collector\nfunc (c *Container) Logs() collector.LogCollector {\n\treturn c.collector.Logs()\n}\n\n// Read metric stream, updating widgets\nfunc (c *Container) Read(stream chan models.Metrics) {\n\tgo func() {\n\t\tfor metrics := range stream {\n\t\t\tc.Metrics = metrics\n\t\t\tc.updater.SetMetrics(metrics)\n\t\t}\n\t\tlog.Infof(\"reader stopped for container: %s\", c.Id)\n\t\tc.Metrics = models.NewMetrics()\n\t\tc.Widgets.Reset()\n\t}()\n\tlog.Infof(\"reader started for container: %s\", c.Id)\n}\n\nfunc (c *Container) Start() {\n\tif c.Meta[\"state\"] != running {\n\t\tif err := c.manager.Start(); err != nil {\n\t\t\tlog.Warningf(\"container %s: %v\", c.Id, err)\n\t\t\tlog.StatusErr(err)\n\t\t\treturn\n\t\t}\n\t\tc.SetState(running)\n\t}\n}\n\nfunc (c *Container) Stop() {\n\tif c.Meta[\"state\"] == running {\n\t\tif err := c.manager.Stop(); err != nil {\n\t\t\tlog.Warningf(\"container %s: %v\", c.Id, err)\n\t\t\tlog.StatusErr(err)\n\t\t\treturn\n\t\t}\n\t\tc.SetState(\"exited\")\n\t}\n}\n\nfunc (c *Container) Remove() {\n\tif err := c.manager.Remove(); err != nil {\n\t\tlog.Warningf(\"container %s: %v\", c.Id, err)\n\t\tlog.StatusErr(err)\n\t}\n}\n\nfunc (c *Container) Pause() {\n\tif c.Meta[\"state\"] == running {\n\t\tif err := c.manager.Pause(); err != nil {\n\t\t\tlog.Warningf(\"container %s: %v\", c.Id, err)\n\t\t\tlog.StatusErr(err)\n\t\t\treturn\n\t\t}\n\t\tc.SetState(\"paused\")\n\t}\n}\n\nfunc (c *Container) Unpause() {\n\tif c.Meta[\"state\"] == \"paused\" {\n\t\tif err := c.manager.Unpause(); err != nil {\n\t\t\tlog.Warningf(\"container %s: %v\", c.Id, err)\n\t\t\tlog.StatusErr(err)\n\t\t\treturn\n\t\t}\n\t\tc.SetState(running)\n\t}\n}\n\nfunc (c *Container) Restart() {\n\tif c.Meta[\"state\"] == running {\n\t\tif err := c.manager.Restart(); err != nil {\n\t\t\tlog.Warningf(\"container %s: %v\", c.Id, err)\n\t\t\tlog.StatusErr(err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *Container) Exec(cmd []string) error {\n\treturn c.manager.Exec(cmd)\n}\n"
  },
  {
    "path": "container/sort.go",
    "content": "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 *Container) bool\n\nvar stateMap = map[string]int{\n\t\"running\": 3,\n\t\"paused\":  2,\n\t\"exited\":  1,\n\t\"created\": 0,\n\t\"\":        0,\n}\n\nvar idSorter = func(c1, c2 *Container) bool { return c1.Id < c2.Id }\nvar nameSorter = func(c1, c2 *Container) bool { return c1.GetMeta(\"name\") < c2.GetMeta(\"name\") }\n\nvar Sorters = map[string]sortMethod{\n\t\"id\":   idSorter,\n\t\"name\": nameSorter,\n\t\"cpu\": func(c1, c2 *Container) bool {\n\t\t// Use secondary sort method if equal values\n\t\tif c1.CPUUtil == c2.CPUUtil {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn c1.CPUUtil > c2.CPUUtil\n\t},\n\t\"mem\": func(c1, c2 *Container) bool {\n\t\t// Use secondary sort method if equal values\n\t\tif c1.MemUsage == c2.MemUsage {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn c1.MemUsage > c2.MemUsage\n\t},\n\t\"mem %\": func(c1, c2 *Container) bool {\n\t\t// Use secondary sort method if equal values\n\t\tif c1.MemPercent == c2.MemPercent {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn c1.MemPercent > c2.MemPercent\n\t},\n\t\"net\": func(c1, c2 *Container) bool {\n\t\tsum1 := sumNet(c1)\n\t\tsum2 := sumNet(c2)\n\t\t// Use secondary sort method if equal values\n\t\tif sum1 == sum2 {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn sum1 > sum2\n\t},\n\t\"pids\": func(c1, c2 *Container) bool {\n\t\t// Use secondary sort method if equal values\n\t\tif c1.Pids == c2.Pids {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn c1.Pids > c2.Pids\n\t},\n\t\"io\": func(c1, c2 *Container) bool {\n\t\tsum1 := sumIO(c1)\n\t\tsum2 := sumIO(c2)\n\t\t// Use secondary sort method if equal values\n\t\tif sum1 == sum2 {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn sum1 > sum2\n\t},\n\t\"state\": func(c1, c2 *Container) bool {\n\t\t// Use secondary sort method if equal values\n\t\tc1state := c1.GetMeta(\"state\")\n\t\tc2state := c2.GetMeta(\"state\")\n\t\tif c1state == c2state {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn stateMap[c1state] > stateMap[c2state]\n\t},\n\t\"uptime\": func(c1, c2 *Container) bool {\n\t\t// Use secondary sort method if equal values\n\t\tc1Uptime := c1.GetMeta(\"uptime\")\n\t\tc2Uptime := c2.GetMeta(\"uptime\")\n\t\tif c1Uptime == c2Uptime {\n\t\t\treturn nameSorter(c1, c2)\n\t\t}\n\t\treturn c1Uptime > c2Uptime\n\t},\n}\n\nfunc SortFields() (fields []string) {\n\tfor k := range Sorters {\n\t\tfields = append(fields, k)\n\t}\n\treturn fields\n}\n\ntype Containers []*Container\n\nfunc (a Containers) Sort()         { sort.Sort(a) }\nfunc (a Containers) Len() int      { return len(a) }\nfunc (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a Containers) Less(i, j int) bool {\n\tf := Sorters[config.GetVal(\"sortField\")]\n\tif config.GetSwitchVal(\"sortReversed\") {\n\t\treturn f(a[j], a[i])\n\t}\n\treturn f(a[i], a[j])\n}\n\nfunc (a Containers) Filter() {\n\tfilter := config.GetVal(\"filterStr\")\n\tre := regexp.MustCompile(fmt.Sprintf(\".*%s\", filter))\n\n\tfor _, c := range a {\n\t\tc.Display = true\n\t\t// Apply name filter\n\t\tif re.FindAllString(c.GetMeta(\"name\"), 1) == nil {\n\t\t\tc.Display = false\n\t\t}\n\t\t// Apply state filter\n\t\tif !config.GetSwitchVal(\"allContainers\") && c.GetMeta(\"state\") != \"running\" {\n\t\t\tc.Display = false\n\t\t}\n\t}\n}\n\nfunc sumNet(c *Container) int64 { return c.NetRx + c.NetTx }\n\nfunc sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }\n"
  },
  {
    "path": "cursor.go",
    "content": "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/gizak/termui\"\n)\n\ntype GridCursor struct {\n\tselectedID  string // id of currently selected container\n\tfiltered    container.Containers\n\tcSuper      *connector.ConnectorSuper\n\tisScrolling bool // toggled when actively scrolling\n}\n\nfunc (gc *GridCursor) Len() int { return len(gc.filtered) }\n\nfunc (gc *GridCursor) Selected() *container.Container {\n\tidx := gc.Idx()\n\tif idx < gc.Len() {\n\t\treturn gc.filtered[idx]\n\t}\n\treturn nil\n}\n\n// Refresh containers from source, returning whether the quantity of\n// containers has changed and any error\nfunc (gc *GridCursor) RefreshContainers() (bool, error) {\n\toldLen := gc.Len()\n\tgc.filtered = container.Containers{}\n\n\tcSource, err := gc.cSuper.Get()\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\t// filter Containers by display bool\n\tvar cursorVisible bool\n\tfor _, c := range cSource.All() {\n\t\tif c.Display {\n\t\t\tif c.Id == gc.selectedID {\n\t\t\t\tcursorVisible = true\n\t\t\t}\n\t\t\tgc.filtered = append(gc.filtered, c)\n\t\t}\n\t}\n\n\tif !cursorVisible || gc.selectedID == \"\" {\n\t\tgc.Reset()\n\t}\n\n\treturn oldLen != gc.Len(), nil\n}\n\n// Set an initial cursor position, if possible\nfunc (gc *GridCursor) Reset() {\n\tcSource, err := gc.cSuper.Get()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, c := range cSource.All() {\n\t\tc.Widgets.UnHighlight()\n\t}\n\tif gc.Len() > 0 {\n\t\tgc.selectedID = gc.filtered[0].Id\n\t\tgc.filtered[0].Widgets.Highlight()\n\t}\n}\n\n// Idx returns current cursor index\nfunc (gc *GridCursor) Idx() int {\n\tfor n, c := range gc.filtered {\n\t\tif c.Id == gc.selectedID {\n\t\t\treturn n\n\t\t}\n\t}\n\tgc.Reset()\n\treturn 0\n}\n\nfunc (gc *GridCursor) ScrollPage() {\n\t// skip scroll if no need to page\n\tif gc.Len() < cGrid.MaxRows() {\n\t\tcGrid.Offset = 0\n\t\treturn\n\t}\n\n\tidx := gc.Idx()\n\n\t// page down\n\tif idx >= cGrid.Offset+cGrid.MaxRows() {\n\t\tcGrid.Offset++\n\t\tcGrid.Align()\n\t}\n\t// page up\n\tif idx < cGrid.Offset {\n\t\tcGrid.Offset--\n\t\tcGrid.Align()\n\t}\n\n}\n\nfunc (gc *GridCursor) Up() {\n\tgc.isScrolling = true\n\tdefer func() { gc.isScrolling = false }()\n\n\tidx := gc.Idx()\n\tif idx <= 0 { // already at top\n\t\treturn\n\t}\n\tactive := gc.filtered[idx]\n\tnext := gc.filtered[idx-1]\n\n\tactive.Widgets.UnHighlight()\n\tgc.selectedID = next.Id\n\tnext.Widgets.Highlight()\n\n\tgc.ScrollPage()\n\tui.Render(cGrid)\n}\n\nfunc (gc *GridCursor) Down() {\n\tgc.isScrolling = true\n\tdefer func() { gc.isScrolling = false }()\n\n\tidx := gc.Idx()\n\tif idx >= gc.Len()-1 { // already at bottom\n\t\treturn\n\t}\n\tactive := gc.filtered[idx]\n\tnext := gc.filtered[idx+1]\n\n\tactive.Widgets.UnHighlight()\n\tgc.selectedID = next.Id\n\tnext.Widgets.Highlight()\n\n\tgc.ScrollPage()\n\tui.Render(cGrid)\n}\n\nfunc (gc *GridCursor) PgUp() {\n\tidx := gc.Idx()\n\tif idx <= 0 { // already at top\n\t\treturn\n\t}\n\n\tnextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))\n\tif gc.pgCount() > 0 {\n\t\tcGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),\n\t\t\tfloat64(0)))\n\t}\n\n\tactive := gc.filtered[idx]\n\tnext := gc.filtered[nextidx]\n\n\tactive.Widgets.UnHighlight()\n\tgc.selectedID = next.Id\n\tnext.Widgets.Highlight()\n\n\tcGrid.Align()\n\tui.Render(cGrid)\n}\n\nfunc (gc *GridCursor) PgDown() {\n\tidx := gc.Idx()\n\tif idx >= gc.Len()-1 { // already at bottom\n\t\treturn\n\t}\n\n\tnextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))\n\tif gc.pgCount() > 0 {\n\t\tcGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),\n\t\t\tfloat64(gc.Len()-cGrid.MaxRows())))\n\t}\n\n\tactive := gc.filtered[idx]\n\tnext := gc.filtered[nextidx]\n\n\tactive.Widgets.UnHighlight()\n\tgc.selectedID = next.Id\n\tnext.Widgets.Highlight()\n\n\tcGrid.Align()\n\tui.Render(cGrid)\n}\n\n// number of pages at current row count and term height\nfunc (gc *GridCursor) pgCount() int {\n\tpages := gc.Len() / cGrid.MaxRows()\n\tif gc.Len()%cGrid.MaxRows() > 0 {\n\t\tpages++\n\t}\n\treturn pages\n}\n"
  },
  {
    "path": "cwidgets/compact/column.go",
    "content": "package compact\n\nimport (\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar (\n\tallCols = map[string]NewCompactColFn{\n\t\t\"status\":  NewStatus,\n\t\t\"name\":    NewNameCol,\n\t\t\"id\":      NewCIDCol,\n\t\t\"image\":   NewImageCol,\n\t\t\"ports\":   NewPortsCol,\n\t\t\"IPs\":     NewIpsCol,\n\t\t\"created\": NewCreatedCol,\n\t\t\"cpu\":     NewCPUCol,\n\t\t\"cpus\":    NewCpuScaledCol,\n\t\t\"mem\":     NewMemCol,\n\t\t\"net\":     NewNetCol,\n\t\t\"io\":      NewIOCol,\n\t\t\"pids\":    NewPIDCol,\n\t\t\"uptime\":  NewUptimeCol,\n\t}\n)\n\ntype NewCompactColFn func() CompactCol\n\nfunc newRowWidgets() []CompactCol {\n\tenabled := config.EnabledColumns()\n\tcols := make([]CompactCol, len(enabled))\n\n\tfor n, name := range enabled {\n\t\twFn, ok := allCols[name]\n\t\tif !ok {\n\t\t\tpanic(\"no such widget name: %s\" + name)\n\t\t}\n\t\tcols[n] = wFn()\n\t}\n\n\treturn cols\n}\n\ntype CompactCol interface {\n\tui.GridBufferer\n\tReset()\n\tHeader() string  // header text to display for column\n\tFixedWidth() int // fixed width size. if == 0, width is automatically calculated\n\tHighlight()\n\tUnHighlight()\n\tSetMeta(models.Meta)\n\tSetMetrics(models.Metrics)\n}\n"
  },
  {
    "path": "cwidgets/compact/gauge.go",
    "content": "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/gizak/termui\"\n)\n\ntype CPUCol struct {\n\t*GaugeCol\n\tscaleCpu bool\n}\n\nfunc NewCPUCol() CompactCol {\n\treturn &CPUCol{NewGaugeCol(\"CPU\"), false}\n}\n\nfunc NewCpuScaledCol() CompactCol {\n\treturn &CPUCol{NewGaugeCol(\"CPUS\"), true}\n}\n\nfunc (w *CPUCol) SetMetrics(m models.Metrics) {\n\tval := m.CPUUtil\n\tw.BarColor = colorScale(val)\n\tif !w.scaleCpu {\n\t\tval = val * int(m.NCpus)\n\t}\n\tw.Label = fmt.Sprintf(\"%d%%\", val)\n\n\tif val > 100 {\n\t\tval = 100\n\t}\n\tw.Percent = val\n}\n\ntype MemCol struct {\n\t*GaugeCol\n}\n\nfunc NewMemCol() CompactCol {\n\treturn &MemCol{NewGaugeCol(\"MEM\")}\n}\n\nfunc (w *MemCol) SetMetrics(m models.Metrics) {\n\tw.BarColor = ui.ThemeAttr(\"gauge.bar.bg\")\n\tw.Label = fmt.Sprintf(\"%s / %s\", cwidgets.ByteFormat64Short(m.MemUsage), cwidgets.ByteFormat64Short(m.MemLimit))\n\tw.Percent = m.MemPercent\n}\n\ntype GaugeCol struct {\n\t*ui.Gauge\n\theader string\n\tfWidth int\n}\n\nfunc NewGaugeCol(header string) *GaugeCol {\n\tg := &GaugeCol{ui.NewGauge(), header, 0}\n\tg.Height = 1\n\tg.Border = false\n\tg.PaddingBottom = 0\n\tg.Reset()\n\treturn g\n}\n\nfunc (w *GaugeCol) Reset() {\n\tw.Label = \"-\"\n\tw.Percent = 0\n}\n\nfunc (w *GaugeCol) Buffer() ui.Buffer {\n\t// if bar would not otherwise be visible, set a minimum\n\t// percentage value and low-contrast color for structure\n\tif w.Percent < 5 {\n\t\tw.Percent = 5\n\t\tw.BarColor = ui.ColorBlack\n\t}\n\n\treturn w.Gauge.Buffer()\n}\n\n// GaugeCol implements CompactCol\nfunc (w *GaugeCol) SetMeta(models.Meta)       {}\nfunc (w *GaugeCol) SetMetrics(models.Metrics) {}\nfunc (w *GaugeCol) Header() string            { return w.header }\nfunc (w *GaugeCol) FixedWidth() int           { return w.fWidth }\n\n// GaugeCol implements CompactCol\nfunc (w *GaugeCol) Highlight() {\n\tw.Bg = ui.ThemeAttr(\"par.text.fg\")\n\tw.PercentColor = ui.ThemeAttr(\"par.text.hi\")\n}\n\n// GaugeCol implements CompactCol\nfunc (w *GaugeCol) UnHighlight() {\n\tw.Bg = ui.ThemeAttr(\"par.text.bg\")\n\tw.PercentColor = ui.ThemeAttr(\"par.text.bg\")\n}\n\nfunc colorScale(n int) ui.Attribute {\n\tif n <= 70 {\n\t\treturn ui.ThemeAttr(\"status.ok\")\n\t}\n\tif n <= 90 {\n\t\treturn ui.ThemeAttr(\"status.warn\")\n\t}\n\treturn ui.ThemeAttr(\"status.danger\")\n}\n"
  },
  {
    "path": "cwidgets/compact/grid.go",
    "content": "package compact\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype CompactGrid struct {\n\tui.GridBufferer\n\theader *CompactHeader\n\tcols   []CompactCol // reference columns\n\tRows   []RowBufferer\n\tX, Y   int\n\tWidth  int\n\tHeight int\n\tOffset int // starting row offset\n}\n\nfunc NewCompactGrid() *CompactGrid {\n\tcg := &CompactGrid{header: NewCompactHeader()}\n\tcg.rebuildHeader()\n\treturn cg\n}\n\nfunc (cg *CompactGrid) Align() {\n\ty := cg.Y\n\n\tif cg.Offset >= len(cg.Rows) || cg.Offset < 0 {\n\t\tcg.Offset = 0\n\t}\n\n\t// update row ypos, width recursively\n\tcolWidths := cg.calcWidths()\n\tfor _, r := range cg.pageRows() {\n\t\tr.SetY(y)\n\t\ty += r.GetHeight()\n\t\tr.SetWidths(cg.Width, colWidths)\n\t}\n}\n\nfunc (cg *CompactGrid) Clear() {\n\tcg.Rows = []RowBufferer{}\n\tcg.rebuildHeader()\n}\n\nfunc (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height }\nfunc (cg *CompactGrid) SetX(x int)     { cg.X = x }\nfunc (cg *CompactGrid) SetY(y int)     { cg.Y = y }\nfunc (cg *CompactGrid) SetWidth(w int) { cg.Width = w }\nfunc (cg *CompactGrid) MaxRows() int   { return ui.TermHeight() - cg.header.Height - cg.Y }\n\n// calculate and return per-column width\nfunc (cg *CompactGrid) calcWidths() []int {\n\tvar autoCols int\n\twidth := cg.Width\n\tcolWidths := make([]int, len(cg.cols))\n\n\tfor n, w := range cg.cols {\n\t\tcolWidths[n] = w.FixedWidth()\n\t\twidth -= w.FixedWidth()\n\t\tif w.FixedWidth() == 0 {\n\t\t\tautoCols++\n\t\t}\n\t}\n\n\tspacing := colSpacing * len(cg.cols)\n\tautoWidth := (width - spacing) / autoCols\n\tfor n, val := range colWidths {\n\t\tif val == 0 {\n\t\t\tcolWidths[n] = autoWidth\n\t\t}\n\t}\n\treturn colWidths\n}\n\nfunc (cg *CompactGrid) pageRows() (rows []RowBufferer) {\n\trows = append(rows, cg.header)\n\trows = append(rows, cg.Rows[cg.Offset:]...)\n\treturn rows\n}\n\nfunc (cg *CompactGrid) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tfor _, r := range cg.pageRows() {\n\t\tbuf.Merge(r.Buffer())\n\t}\n\treturn buf\n}\n\nfunc (cg *CompactGrid) AddRows(rows ...RowBufferer) {\n\tcg.Rows = append(cg.Rows, rows...)\n}\n\nfunc (cg *CompactGrid) rebuildHeader() {\n\tcg.cols = newRowWidgets()\n\tcg.header.clearFieldPars()\n\tfor _, col := range cg.cols {\n\t\tcg.header.addFieldPar(col.Header())\n\t}\n}\n"
  },
  {
    "path": "cwidgets/compact/header.go",
    "content": "package compact\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype CompactHeader struct {\n\tX, Y   int\n\tWidth  int\n\tHeight int\n\tcols   []CompactCol\n\twidths []int\n\tpars   []*ui.Par\n}\n\nfunc NewCompactHeader() *CompactHeader {\n\treturn &CompactHeader{\n\t\tX:      rowPadding,\n\t\tHeight: 2,\n\t}\n}\n\nfunc (row *CompactHeader) GetHeight() int {\n\treturn row.Height\n}\n\nfunc (row *CompactHeader) SetWidths(totalWidth int, widths []int) {\n\tx := row.X\n\n\tfor n, w := range row.pars {\n\t\tw.SetX(x)\n\t\tw.SetWidth(widths[n])\n\t\tx += widths[n] + colSpacing\n\t}\n\trow.Width = totalWidth\n}\n\nfunc (row *CompactHeader) SetX(x int) {\n\trow.X = x\n}\n\nfunc (row *CompactHeader) SetY(y int) {\n\tfor _, p := range row.pars {\n\t\tp.SetY(y)\n\t}\n\trow.Y = y\n}\n\nfunc (row *CompactHeader) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tfor _, p := range row.pars {\n\t\tbuf.Merge(p.Buffer())\n\t}\n\treturn buf\n}\n\nfunc (row *CompactHeader) clearFieldPars() {\n\trow.pars = []*ui.Par{}\n}\n\nfunc (row *CompactHeader) addFieldPar(s string) {\n\tp := ui.NewPar(s)\n\tp.Height = row.Height\n\tp.Border = false\n\trow.pars = append(row.pars, p)\n}\n"
  },
  {
    "path": "cwidgets/compact/row.go",
    "content": "package compact\n\nimport (\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nconst rowPadding = 1\n\nvar log = logging.Init()\n\ntype RowBufferer interface {\n\tSetY(int)\n\tSetWidths(int, []int)\n\tGetHeight() int\n\tBuffer() ui.Buffer\n}\n\ntype CompactRow struct {\n\tBg     *RowBg\n\tCols   []CompactCol\n\tX, Y   int\n\tHeight int\n\twidths []int // column widths\n}\n\nfunc NewCompactRow() *CompactRow {\n\trow := &CompactRow{\n\t\tBg:     NewRowBg(),\n\t\tCols:   newRowWidgets(),\n\t\tX:      rowPadding,\n\t\tHeight: 1,\n\t}\n\n\treturn row\n}\n\nfunc (row *CompactRow) SetMeta(m models.Meta) {\n\tfor _, w := range row.Cols {\n\t\tw.SetMeta(m)\n\t}\n}\n\nfunc (row *CompactRow) SetMetrics(m models.Metrics) {\n\tfor _, w := range row.Cols {\n\t\tw.SetMetrics(m)\n\t}\n}\n\n// Set gauges, counters, etc. to default unread values\nfunc (row *CompactRow) Reset() {\n\tfor _, w := range row.Cols {\n\t\tw.Reset()\n\t}\n}\n\nfunc (row *CompactRow) GetHeight() int { return row.Height }\n\n//func (row *CompactRow) SetX(x int)     { row.X = x }\n\nfunc (row *CompactRow) SetY(y int) {\n\tif y == row.Y {\n\t\treturn\n\t}\n\n\trow.Bg.Y = y\n\tfor _, w := range row.Cols {\n\t\tw.SetY(y)\n\t}\n\trow.Y = y\n}\n\nfunc (row *CompactRow) SetWidths(totalWidth int, widths []int) {\n\tx := row.X\n\n\trow.Bg.SetX(x)\n\trow.Bg.SetWidth(totalWidth)\n\n\tfor n, w := range row.Cols {\n\t\tw.SetX(x)\n\t\tw.SetWidth(widths[n])\n\t\tx += widths[n] + colSpacing\n\t}\n}\n\nfunc (row *CompactRow) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tbuf.Merge(row.Bg.Buffer())\n\tfor _, w := range row.Cols {\n\t\tbuf.Merge(w.Buffer())\n\t}\n\treturn buf\n}\n\nfunc (row *CompactRow) Highlight() {\n\trow.Cols[1].Highlight()\n\tif config.GetSwitchVal(\"fullRowCursor\") {\n\t\tfor _, w := range row.Cols {\n\t\t\tw.Highlight()\n\t\t}\n\t}\n}\n\nfunc (row *CompactRow) UnHighlight() {\n\trow.Cols[1].UnHighlight()\n\tif config.GetSwitchVal(\"fullRowCursor\") {\n\t\tfor _, w := range row.Cols {\n\t\t\tw.UnHighlight()\n\t\t}\n\t}\n}\n\ntype RowBg struct {\n\t*ui.Par\n}\n\nfunc NewRowBg() *RowBg {\n\tbg := ui.NewPar(\"\")\n\tbg.Height = 1\n\tbg.Border = false\n\tbg.Bg = ui.ThemeAttr(\"par.text.bg\")\n\treturn &RowBg{bg}\n}\n\nfunc (w *RowBg) Highlight()   { w.Bg = ui.ThemeAttr(\"par.text.fg\") }\nfunc (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr(\"par.text.bg\") }\n"
  },
  {
    "path": "cwidgets/compact/status.go",
    "content": "package compact\n\nimport (\n\t\"github.com/bcicen/ctop/models\"\n\n\tui \"github.com/gizak/termui\"\n)\n\n// Status indicator\ntype Status struct {\n\t*ui.Block\n\tstatus []ui.Cell\n\thealth []ui.Cell\n}\n\nfunc NewStatus() CompactCol {\n\ts := &Status{\n\t\tBlock:  ui.NewBlock(),\n\t\tstatus: []ui.Cell{{Ch: ' '}},\n\t\thealth: []ui.Cell{{Ch: ' '}},\n\t}\n\ts.Height = 1\n\ts.Border = false\n\treturn s\n}\n\nfunc (s *Status) Buffer() ui.Buffer {\n\tbuf := s.Block.Buffer()\n\tbuf.Set(s.InnerX(), s.InnerY(), s.health[0])\n\tbuf.Set(s.InnerX()+2, s.InnerY(), s.status[0])\n\treturn buf\n}\n\nfunc (s *Status) SetMeta(m models.Meta) {\n\ts.setState(m.Get(\"state\"))\n\ts.setHealth(m.Get(\"health\"))\n}\n\n// Status implements CompactCol\nfunc (s *Status) Reset()                    {}\nfunc (s *Status) SetMetrics(models.Metrics) {}\nfunc (s *Status) Highlight()                {}\nfunc (s *Status) UnHighlight()              {}\nfunc (s *Status) Header() string            { return \"\" }\nfunc (s *Status) FixedWidth() int           { return 3 }\n\nfunc (s *Status) setState(val string) {\n\tcolor := ui.ColorDefault\n\tvar mark string\n\n\tswitch val {\n\tcase \"\":\n\t\treturn\n\tcase \"created\":\n\t\tmark = \"◉\"\n\tcase \"running\":\n\t\tmark = \"▶\"\n\t\tcolor = ui.ThemeAttr(\"status.ok\")\n\tcase \"exited\":\n\t\tmark = \"⏹\"\n\t\tcolor = ui.ThemeAttr(\"status.danger\")\n\tcase \"paused\":\n\t\tmark = \"⏸\"\n\tdefault:\n\t\tmark = \" \"\n\t\tlog.Warningf(\"unknown status string: \\\"%v\\\"\", val)\n\t}\n\n\ts.status = ui.TextCells(mark, color, ui.ColorDefault)\n}\n\nfunc (s *Status) setHealth(val string) {\n\tcolor := ui.ColorDefault\n\tvar mark string\n\n\tswitch val {\n\tcase \"\":\n\t\treturn\n\tcase \"healthy\":\n\t\tmark = \"☼\"\n\t\tcolor = ui.ThemeAttr(\"status.ok\")\n\tcase \"unhealthy\":\n\t\tmark = \"⚠\"\n\t\tcolor = ui.ThemeAttr(\"status.danger\")\n\tcase \"starting\":\n\t\tmark = \"◌\"\n\t\tcolor = ui.ThemeAttr(\"status.warn\")\n\tdefault:\n\t\tmark = \" \"\n\t\tlog.Warningf(\"unknown health state string: \\\"%v\\\"\", val)\n\t}\n\n\ts.health = ui.TextCells(mark, color, ui.ColorDefault)\n}\n"
  },
  {
    "path": "cwidgets/compact/text.go",
    "content": "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/gizak/termui\"\n)\n\n// Column that shows container's meta property i.e. name, id, image tc.\ntype MetaCol struct {\n\t*TextCol\n\tmetaName string\n}\n\nfunc (w *MetaCol) SetMeta(m models.Meta) {\n\tw.setText(m.Get(w.metaName))\n}\n\nfunc NewNameCol() CompactCol {\n\tc := &MetaCol{NewTextCol(\"NAME\"), \"name\"}\n\tc.fWidth = 30\n\treturn c\n}\n\nfunc NewCIDCol() CompactCol {\n\tc := &MetaCol{NewTextCol(\"CID\"), \"id\"}\n\tc.fWidth = 12\n\treturn c\n}\n\nfunc NewImageCol() CompactCol {\n\treturn &MetaCol{NewTextCol(\"IMAGE\"), \"image\"}\n}\n\nfunc NewPortsCol() CompactCol {\n\treturn &MetaCol{NewTextCol(\"PORTS\"), \"ports\"}\n}\n\nfunc NewIpsCol() CompactCol {\n\treturn &MetaCol{NewTextCol(\"IPs\"), \"IPs\"}\n}\n\nfunc NewCreatedCol() CompactCol {\n\tc := &MetaCol{NewTextCol(\"CREATED\"), \"created\"}\n\tc.fWidth = 19 // Year will be stripped e.g. \"Thu Nov 26 07:44:03\" without 2020 at end\n\treturn c\n}\n\ntype NetCol struct {\n\t*TextCol\n}\n\nfunc NewNetCol() CompactCol {\n\treturn &NetCol{NewTextCol(\"NET RX/TX\")}\n}\n\nfunc (w *NetCol) SetMetrics(m models.Metrics) {\n\tlabel := fmt.Sprintf(\"%s / %s\", cwidgets.ByteFormat64Short(m.NetRx), cwidgets.ByteFormat64Short(m.NetTx))\n\tw.setText(label)\n}\n\ntype IOCol struct {\n\t*TextCol\n}\n\nfunc NewIOCol() CompactCol {\n\treturn &IOCol{NewTextCol(\"IO R/W\")}\n}\n\nfunc (w *IOCol) SetMetrics(m models.Metrics) {\n\tlabel := fmt.Sprintf(\"%s / %s\", cwidgets.ByteFormat64Short(m.IOBytesRead), cwidgets.ByteFormat64Short(m.IOBytesWrite))\n\tw.setText(label)\n}\n\ntype PIDCol struct {\n\t*TextCol\n}\n\nfunc NewPIDCol() CompactCol {\n\tw := &PIDCol{NewTextCol(\"PIDS\")}\n\tw.fWidth = 4\n\treturn w\n}\n\nfunc (w *PIDCol) SetMetrics(m models.Metrics) {\n\tw.setText(fmt.Sprintf(\"%d\", m.Pids))\n}\n\ntype UptimeCol struct {\n\t*TextCol\n}\n\nfunc NewUptimeCol() CompactCol {\n\treturn &UptimeCol{NewTextCol(\"UPTIME\")}\n}\n\nfunc (w *UptimeCol) SetMeta(m models.Meta) {\n\tw.Text = m.Get(\"uptime\")\n}\n\ntype TextCol struct {\n\t*ui.Par\n\theader string\n\tfWidth int\n}\n\nfunc NewTextCol(header string) *TextCol {\n\tp := ui.NewPar(\"-\")\n\tp.Border = false\n\tp.Height = 1\n\tp.Width = 20\n\n\treturn &TextCol{\n\t\tPar:    p,\n\t\theader: header,\n\t\tfWidth: 0,\n\t}\n}\n\nfunc (w *TextCol) Highlight() {\n\tw.Bg = ui.ThemeAttr(\"par.text.fg\")\n\tw.TextFgColor = ui.ThemeAttr(\"par.text.hi\")\n\tw.TextBgColor = ui.ThemeAttr(\"par.text.fg\")\n}\n\nfunc (w *TextCol) UnHighlight() {\n\tw.Bg = ui.ThemeAttr(\"par.text.bg\")\n\tw.TextFgColor = ui.ThemeAttr(\"par.text.fg\")\n\tw.TextBgColor = ui.ThemeAttr(\"par.text.bg\")\n}\n\n// TextCol implements CompactCol\nfunc (w *TextCol) Reset()                    { w.setText(\"-\") }\nfunc (w *TextCol) SetMeta(models.Meta)       {}\nfunc (w *TextCol) SetMetrics(models.Metrics) {}\nfunc (w *TextCol) Header() string            { return w.header }\nfunc (w *TextCol) FixedWidth() int           { return w.fWidth }\n\nfunc (w *TextCol) setText(s string) {\n\tif w.fWidth > 0 && len(s) > w.fWidth {\n\t\ts = s[0:w.fWidth]\n\t}\n\tw.Text = s\n}\n"
  },
  {
    "path": "cwidgets/compact/util.go",
    "content": "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\nfunc centerParText(p *ui.Par) {\n\tvar text string\n\tvar padding string\n\n\t// strip existing left-padding\n\tfor i, ch := range p.Text {\n\t\tif string(ch) != \" \" {\n\t\t\ttext = p.Text[i:]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tpadlen := (p.InnerWidth() - len(text)) / 2\n\tfor i := 0; i < padlen; i++ {\n\t\tpadding += \" \"\n\t}\n\tp.Text = fmt.Sprintf(\"%s%s\", padding, text)\n}\n"
  },
  {
    "path": "cwidgets/main.go",
    "content": "package cwidgets\n\nimport (\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n)\n\nvar log = logging.Init()\n\ntype WidgetUpdater interface {\n\tSetMeta(models.Meta)\n\tSetMetrics(models.Metrics)\n}\n\ntype NullWidgetUpdater struct{}\n\n// NullWidgetUpdater implements WidgetUpdater\nfunc (wu NullWidgetUpdater) SetMeta(models.Meta) {}\n\n// NullWidgetUpdater implements WidgetUpdater\nfunc (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}\n"
  },
  {
    "path": "cwidgets/single/cpu.go",
    "content": "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 NewCpu() *Cpu {\n\tcpu := &Cpu{ui.NewLineChart(), NewFloatHist(55)}\n\tcpu.Mode = \"dot\"\n\tcpu.BorderLabel = \"CPU\"\n\tcpu.Height = 12\n\tcpu.Width = colWidth[0]\n\tcpu.X = 0\n\tcpu.DataLabels = cpu.hist.Labels\n\n\t// hack to force the default minY scale to 0\n\ttmpData := []float64{20}\n\tcpu.Data[\"CPU\"] = tmpData\n\t_ = cpu.Buffer()\n\n\tcpu.Data[\"CPU\"] = cpu.hist.Data\n\treturn cpu\n}\n\nfunc (w *Cpu) Update(val int) {\n\tw.hist.Append(float64(val))\n}\n"
  },
  {
    "path": "cwidgets/single/env.go",
    "content": "package single\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar envPattern = regexp.MustCompile(`(?P<KEY>[^=]+)=(?P<VALUJE>.*)`)\n\ntype Env struct {\n\t*ui.Table\n\tdata map[string]string\n}\n\nfunc NewEnv() *Env {\n\tp := ui.NewTable()\n\tp.Height = 4\n\tp.Width = colWidth[0]\n\tp.FgColor = ui.ThemeAttr(\"par.text.fg\")\n\tp.Separator = false\n\ti := &Env{p, make(map[string]string)}\n\ti.BorderLabel = \"Env\"\n\treturn i\n}\n\nfunc (w *Env) Set(allEnvs string) {\n\tenvs := strings.Split(allEnvs, \";\")\n\tw.Rows = [][]string{}\n\tfor _, env := range envs {\n\t\tmatch := envPattern.FindStringSubmatch(env)\n\t\tif len(match) == 3 {\n\t\t\tkey := match[1]\n\t\t\tvalue := match[2]\n\t\t\tw.data[key] = value\n\t\t\tw.Rows = append(w.Rows, mkInfoRows(key, value)...)\n\t\t}\n\t}\n\n\tw.Height = len(w.Rows) + 2\n}\n"
  },
  {
    "path": "cwidgets/single/hist.go",
    "content": "package single\n\ntype IntHist struct {\n\tVal    int   // most current data point\n\tData   []int // historical data points\n\tLabels []string\n}\n\nfunc NewIntHist(max int) *IntHist {\n\treturn &IntHist{\n\t\tData:   make([]int, max),\n\t\tLabels: make([]string, max),\n\t}\n}\n\nfunc (h *IntHist) Append(val int) {\n\tif len(h.Data) == cap(h.Data) {\n\t\th.Data = append(h.Data[:0], h.Data[1:]...)\n\t}\n\th.Val = val\n\th.Data = append(h.Data, val)\n}\n\ntype DiffHist struct {\n\t*IntHist\n\tlastVal int\n}\n\nfunc NewDiffHist(max int) *DiffHist {\n\treturn &DiffHist{NewIntHist(max), -1}\n}\n\nfunc (h *DiffHist) Append(val int) {\n\tif h.lastVal >= 0 { // skip append if this is the initial update\n\t\tdiff := val - h.lastVal\n\t\th.IntHist.Append(diff)\n\t}\n\th.lastVal = val\n}\n\ntype FloatHist struct {\n\tVal    float64   // most current data point\n\tData   []float64 // historical data points\n\tLabels []string\n}\n\nfunc NewFloatHist(max int) FloatHist {\n\treturn FloatHist{\n\t\tData:   make([]float64, max),\n\t\tLabels: make([]string, max),\n\t}\n}\n\nfunc (h FloatHist) Append(val float64) {\n\tif len(h.Data) == cap(h.Data) {\n\t\th.Data = append(h.Data[:0], h.Data[1:]...)\n\t}\n\th.Val = val\n\th.Data = append(h.Data, val)\n}\n"
  },
  {
    "path": "cwidgets/single/info.go",
    "content": "package single\n\nimport (\n\t\"strings\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar displayInfo = []string{\"id\", \"name\", \"image\", \"ports\", \"IPs\", \"state\", \"created\", \"uptime\", \"health\"}\n\ntype Info struct {\n\t*ui.Table\n\tdata map[string]string\n}\n\nfunc NewInfo() *Info {\n\tp := ui.NewTable()\n\tp.Height = 4\n\tp.Width = colWidth[0]\n\tp.FgColor = ui.ThemeAttr(\"par.text.fg\")\n\tp.Separator = false\n\ti := &Info{p, make(map[string]string)}\n\treturn i\n}\n\nfunc (w *Info) Set(k, v string) {\n\tw.data[k] = v\n\n\t// rebuild rows\n\tw.Rows = [][]string{}\n\tfor _, k := range displayInfo {\n\t\tif v, ok := w.data[k]; ok {\n\t\t\tw.Rows = append(w.Rows, mkInfoRows(k, v)...)\n\t\t}\n\t}\n\n\tw.Height = len(w.Rows) + 2\n}\n\n// Build row(s) from a key and value string\nfunc mkInfoRows(k, v string) (rows [][]string) {\n\tlines := strings.Split(v, \"\\n\")\n\n\t// initial row with field name\n\trows = append(rows, []string{k, lines[0]})\n\n\t// append any additional lines in separate row\n\tif len(lines) > 1 {\n\t\tfor _, line := range lines[1:] {\n\t\t\tif line != \"\" {\n\t\t\t\trows = append(rows, []string{\"\", line})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn rows\n}\n"
  },
  {
    "path": "cwidgets/single/io.go",
    "content": "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 struct {\n\t*ui.Sparklines\n\treadHist  *DiffHist\n\twriteHist *DiffHist\n}\n\nfunc NewIO() *IO {\n\tio := &IO{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}\n\tio.BorderLabel = \"IO\"\n\tio.Height = 6\n\tio.Width = colWidth[0]\n\tio.X = 0\n\tio.Y = 24\n\n\tread := ui.NewSparkline()\n\tread.Title = \"READ\"\n\tread.Height = 1\n\tread.Data = io.readHist.Data\n\tread.LineColor = ui.ColorGreen\n\n\twrite := ui.NewSparkline()\n\twrite.Title = \"WRITE\"\n\twrite.Height = 1\n\twrite.Data = io.writeHist.Data\n\twrite.LineColor = ui.ColorYellow\n\n\tio.Lines = []ui.Sparkline{read, write}\n\treturn io\n}\n\nfunc (w *IO) Update(read int64, write int64) {\n\tvar rate string\n\n\tw.readHist.Append(int(read))\n\trate = strings.ToLower(cwidgets.ByteFormatShort(w.readHist.Val))\n\tw.Lines[0].Title = fmt.Sprintf(\"read [%s/s]\", rate)\n\n\tw.writeHist.Append(int(write))\n\trate = strings.ToLower(cwidgets.ByteFormatShort(w.writeHist.Val))\n\tw.Lines[1].Title = fmt.Sprintf(\"write [%s/s]\", rate)\n}\n"
  },
  {
    "path": "cwidgets/single/logs.go",
    "content": "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 {\n\tts   []time.Time\n\tdata []string\n}\n\nfunc NewLogLines(max int) *LogLines {\n\tll := &LogLines{\n\t\tts:   make([]time.Time, max),\n\t\tdata: make([]string, max),\n\t}\n\treturn ll\n}\n\nfunc (ll *LogLines) tail(n int) []string {\n\tlines := make([]string, n)\n\tfor i := 0; i < n; i++ {\n\t\tlines = append(lines, ll.data[len(ll.data)-i])\n\t}\n\treturn lines\n}\nfunc (ll *LogLines) getLines(start, end int) []string {\n\tif end < 0 {\n\t\treturn ll.data[start:]\n\t}\n\treturn ll.data[start:end]\n}\n\nfunc (ll *LogLines) add(l models.Log) {\n\tif len(ll.data) == cap(ll.data) {\n\t\tll.data = append(ll.data[:0], ll.data[1:]...)\n\t\tll.ts = append(ll.ts[:0], ll.ts[1:]...)\n\t}\n\tll.ts = append(ll.ts, l.Timestamp)\n\tll.data = append(ll.data, l.Message)\n\tlog.Debugf(\"recorded log line: %v\", l)\n}\n\ntype Logs struct {\n\t*ui.List\n\tlines *LogLines\n}\n\nfunc NewLogs(stream chan models.Log) *Logs {\n\tp := ui.NewList()\n\tp.Y = ui.TermHeight() / 2\n\tp.X = 0\n\tp.Height = ui.TermHeight() - p.Y\n\tp.Width = ui.TermWidth()\n\t//p.Overflow = \"wrap\"\n\tp.ItemFgColor = ui.ThemeAttr(\"par.text.fg\")\n\ti := &Logs{p, NewLogLines(4098)}\n\tgo func() {\n\t\tfor line := range stream {\n\t\t\ti.lines.add(line)\n\t\t\tui.Render(i)\n\t\t}\n\t}()\n\treturn i\n}\n\nfunc (w *Logs) Align() {\n\tw.X = colWidth[0]\n\tw.List.Align()\n}\n\nfunc (w *Logs) Buffer() ui.Buffer {\n\tmaxLines := w.Height - 2\n\toffset := len(w.lines.data) - maxLines\n\tw.Items = w.lines.getLines(offset, -1)\n\treturn w.List.Buffer()\n}\n\n// number of rows a line will occupy at current panel width\nfunc (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }\n"
  },
  {
    "path": "cwidgets/single/main.go",
    "content": "package single\n\nimport (\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/models\"\n\tui \"github.com/gizak/termui\"\n)\n\nvar (\n\tlog       = logging.Init()\n\tsizeError = termSizeError()\n\tcolWidth  = [2]int{65, 0} // left,right column width\n)\n\ntype Single struct {\n\tInfo  *Info\n\tNet   *Net\n\tCpu   *Cpu\n\tMem   *Mem\n\tIO    *IO\n\tEnv   *Env\n\tX, Y  int\n\tWidth int\n}\n\nfunc NewSingle() *Single {\n\treturn &Single{\n\t\tInfo:  NewInfo(),\n\t\tNet:   NewNet(),\n\t\tCpu:   NewCpu(),\n\t\tMem:   NewMem(),\n\t\tIO:    NewIO(),\n\t\tEnv:   NewEnv(),\n\t\tWidth: ui.TermWidth(),\n\t}\n}\n\nfunc (e *Single) Up() {\n\tif e.Y < 0 {\n\t\te.Y++\n\t\te.Align()\n\t\tui.Render(e)\n\t}\n}\n\nfunc (e *Single) Down() {\n\tif e.Y > (ui.TermHeight() - e.GetHeight()) {\n\t\te.Y--\n\t\te.Align()\n\t\tui.Render(e)\n\t}\n}\n\nfunc (e *Single) SetWidth(w int) { e.Width = w }\nfunc (e *Single) SetMeta(m models.Meta) {\n\tfor k, v := range m {\n\t\tif k == \"[ENV-VAR]\" {\n\t\t\te.Env.Set(v)\n\t\t} else {\n\t\t\te.Info.Set(k, v)\n\t\t}\n\t}\n}\n\nfunc (e *Single) SetMetrics(m models.Metrics) {\n\te.Cpu.Update(m.CPUUtil)\n\te.Net.Update(m.NetRx, m.NetTx)\n\te.Mem.Update(int(m.MemUsage), int(m.MemLimit))\n\te.IO.Update(m.IOBytesRead, m.IOBytesWrite)\n}\n\n// GetHeight returns total column height\nfunc (e *Single) GetHeight() (h int) {\n\th += e.Info.Height\n\th += e.Net.Height\n\th += e.Cpu.Height\n\th += e.Mem.Height\n\th += e.IO.Height\n\th += e.Env.Height\n\treturn h\n}\n\nfunc (e *Single) Align() {\n\t// reset offset if needed\n\tif e.GetHeight() <= ui.TermHeight() {\n\t\te.Y = 0\n\t}\n\n\ty := e.Y\n\tfor _, i := range e.all() {\n\t\ti.SetY(y)\n\t\ty += i.GetHeight()\n\t}\n\n\tif e.Width > colWidth[0] {\n\t\tcolWidth[1] = e.Width - (colWidth[0] + 1)\n\t}\n\te.Mem.Align()\n\tlog.Debugf(\"align: width=%v left-col=%v right-col=%v\", e.Width, colWidth[0], colWidth[1])\n}\n\nfunc (e *Single) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tif e.Width < (colWidth[0] + colWidth[1]) {\n\t\tui.Clear()\n\t\tbuf.Merge(sizeError.Buffer())\n\t\treturn buf\n\t}\n\tbuf.Merge(e.Info.Buffer())\n\tbuf.Merge(e.Cpu.Buffer())\n\tbuf.Merge(e.Mem.Buffer())\n\tbuf.Merge(e.Net.Buffer())\n\tbuf.Merge(e.IO.Buffer())\n\tbuf.Merge(e.Env.Buffer())\n\treturn buf\n}\n\nfunc (e *Single) all() []ui.GridBufferer {\n\treturn []ui.GridBufferer{\n\t\te.Info,\n\t\te.Cpu,\n\t\te.Mem,\n\t\te.Net,\n\t\te.IO,\n\t\te.Env,\n\t}\n}\n\nfunc termSizeError() *ui.Par {\n\tp := ui.NewPar(\"screen too small!\")\n\tp.Height = 1\n\tp.Width = 20\n\tp.Border = false\n\treturn p\n}\n"
  },
  {
    "path": "cwidgets/single/mem.go",
    "content": "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*ui.Block\n\tChart      *ui.MBarChart\n\tInnerLabel *ui.Par\n\tvalHist    *IntHist\n\tlimitHist  *IntHist\n}\n\nfunc NewMem() *Mem {\n\tmem := &Mem{\n\t\tBlock:      ui.NewBlock(),\n\t\tChart:      newMemChart(),\n\t\tInnerLabel: newMemLabel(),\n\t\tvalHist:    NewIntHist(9),\n\t\tlimitHist:  NewIntHist(9),\n\t}\n\tmem.Height = 13\n\tmem.Width = colWidth[0]\n\tmem.BorderLabel = \"MEM\"\n\n\tmem.Chart.Data[0] = mem.valHist.Data\n\tmem.Chart.Data[1] = mem.limitHist.Data\n\tmem.Chart.DataLabels = mem.valHist.Labels\n\n\treturn mem\n}\n\nfunc (w *Mem) Align() {\n\ty := w.Y + 1\n\tw.InnerLabel.SetY(y)\n\tw.Chart.SetY(y + w.InnerLabel.Height)\n\n\tw.Chart.Height = w.Height - w.InnerLabel.Height - 2\n\tw.Chart.SetWidth(w.Width - 2)\n}\n\nfunc (w *Mem) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tbuf.Merge(w.Block.Buffer())\n\tbuf.Merge(w.InnerLabel.Buffer())\n\tbuf.Merge(w.Chart.Buffer())\n\treturn buf\n}\n\nfunc newMemLabel() *ui.Par {\n\tp := ui.NewPar(\"-\")\n\tp.X = 1\n\tp.Border = false\n\tp.Height = 1\n\tp.Width = 20\n\treturn p\n}\n\nfunc newMemChart() *ui.MBarChart {\n\tmbar := ui.NewMBarChart()\n\tmbar.X = 1\n\tmbar.Border = false\n\tmbar.BarGap = 1\n\tmbar.BarWidth = 6\n\n\tmbar.BarColor[1] = ui.ColorBlack\n\tmbar.NumColor[1] = ui.ColorBlack\n\n\tmbar.NumFmt = cwidgets.ByteFormatShort\n\t//mbar.ShowScale = true\n\treturn mbar\n}\n\nfunc (w *Mem) Update(val int, limit int) {\n\tw.valHist.Append(val)\n\tw.limitHist.Append(limit - val)\n\tw.InnerLabel.Text = fmt.Sprintf(\"%v / %v\", cwidgets.ByteFormatShort(val), cwidgets.ByteFormatShort(limit))\n\t//w.Data[0] = w.hist.data\n}\n"
  },
  {
    "path": "cwidgets/single/net.go",
    "content": "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 struct {\n\t*ui.Sparklines\n\trxHist *DiffHist\n\ttxHist *DiffHist\n}\n\nfunc NewNet() *Net {\n\tnet := &Net{ui.NewSparklines(), NewDiffHist(60), NewDiffHist(60)}\n\tnet.BorderLabel = \"NET\"\n\tnet.Height = 6\n\tnet.Width = colWidth[0]\n\tnet.X = 0\n\tnet.Y = 24\n\n\trx := ui.NewSparkline()\n\trx.Title = \"RX\"\n\trx.Height = 1\n\trx.Data = net.rxHist.Data\n\trx.LineColor = ui.ColorGreen\n\n\ttx := ui.NewSparkline()\n\ttx.Title = \"TX\"\n\ttx.Height = 1\n\ttx.Data = net.txHist.Data\n\ttx.LineColor = ui.ColorYellow\n\n\tnet.Lines = []ui.Sparkline{rx, tx}\n\treturn net\n}\n\nfunc (w *Net) Update(rx int64, tx int64) {\n\tvar rate string\n\n\tw.rxHist.Append(int(rx))\n\trate = strings.ToLower(cwidgets.ByteFormat(w.rxHist.Val))\n\tw.Lines[0].Title = fmt.Sprintf(\"RX [%s/s]\", rate)\n\n\tw.txHist.Append(int(tx))\n\trate = strings.ToLower(cwidgets.ByteFormat(w.txHist.Val))\n\tw.Lines[1].Title = fmt.Sprintf(\"TX [%s/s]\", rate)\n}\n"
  },
  {
    "path": "cwidgets/util.go",
    "content": "package cwidgets\n\nimport (\n\t\"strconv\"\n)\n\nconst (\n\t// byte ratio constants\n\t_           = iota\n\tkib float64 = 1 << (10 * iota)\n\tmib\n\tgib\n\ttib\n\tpib\n)\n\nvar (\n\tunits = []float64{\n\t\t1,\n\t\tkib,\n\t\tmib,\n\t\tgib,\n\t\ttib,\n\t\tpib,\n\t}\n\n\t// short, full unit labels\n\tlabels = [][2]string{\n\t\t[2]string{\"B\", \"B\"},\n\t\t[2]string{\"K\", \"KiB\"},\n\t\t[2]string{\"M\", \"MiB\"},\n\t\t[2]string{\"G\", \"GiB\"},\n\t\t[2]string{\"T\", \"TiB\"},\n\t\t[2]string{\"P\", \"PiB\"},\n\t}\n)\n\n// convenience methods\nfunc ByteFormat(n int) string          { return byteFormat(float64(n), false) }\nfunc ByteFormatShort(n int) string     { return byteFormat(float64(n), true) }\nfunc ByteFormat64(n int64) string      { return byteFormat(float64(n), false) }\nfunc ByteFormat64Short(n int64) string { return byteFormat(float64(n), true) }\n\nfunc byteFormat(n float64, short bool) string {\n\ti := len(units) - 1\n\n\tfor i > 0 {\n\t\tif n >= units[i] {\n\t\t\tn /= units[i]\n\t\t\tbreak\n\t\t}\n\t\ti--\n\t}\n\n\tif short {\n\t\treturn unpadFloat(n, 0) + labels[i][0]\n\t}\n\treturn unpadFloat(n, 2) + labels[i][1]\n}\n\nfunc unpadFloat(f float64, maxp int) string {\n\treturn strconv.FormatFloat(f, 'f', getPrecision(f, maxp), 64)\n}\n\nfunc getPrecision(f float64, maxp int) int {\n\tfrac := int((f - float64(int(f))) * 100)\n\tif frac == 0 || maxp == 0 {\n\t\treturn 0\n\t}\n\tif frac%10 == 0 || maxp < 2 {\n\t\treturn 1\n\t}\n\treturn maxp\n}\n"
  },
  {
    "path": "debug.go",
    "content": "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)\n\nvar mstats = &runtime.MemStats{}\n\nfunc logEvent(e ui.Event) {\n\t// skip timer events e.g. /timer/1s\n\tif e.From == \"timer\" {\n\t\treturn\n\t}\n\tvar s string\n\ts += fmt.Sprintf(\"Type=%s\", quote(e.Type))\n\ts += fmt.Sprintf(\" Path=%s\", quote(e.Path))\n\ts += fmt.Sprintf(\" From=%s\", quote(e.From))\n\tif e.To != \"\" {\n\t\ts += fmt.Sprintf(\" To=%s\", quote(e.To))\n\t}\n\tlog.Debugf(\"new event: %s\", s)\n}\n\nfunc runtimeStats() {\n\tvar msg string\n\tmsg += fmt.Sprintf(\"cgo calls=%v\", runtime.NumCgoCall())\n\tmsg += fmt.Sprintf(\" routines=%v\", runtime.NumGoroutine())\n\truntime.ReadMemStats(mstats)\n\tmsg += fmt.Sprintf(\" numgc=%v\", mstats.NumGC)\n\tmsg += fmt.Sprintf(\" alloc=%v\", mstats.Alloc)\n\tlog.Debugf(\"runtime: %v\", msg)\n}\n\nfunc runtimeStack() {\n\tbuf := make([]byte, 32768)\n\tbuf = buf[:runtime.Stack(buf, true)]\n\tlog.Infof(fmt.Sprintf(\"stack:\\n%v\", string(buf)))\n}\n\n// log container, metrics, and widget state\nfunc dumpContainer(c *container.Container) {\n\tmsg := fmt.Sprintf(\"logging state for container: %s\\n\", c.Id)\n\tfor k, v := range c.Meta {\n\t\tmsg += fmt.Sprintf(\"Meta.%s = %s\\n\", k, v)\n\t}\n\tmsg += inspect(&c.Metrics)\n\tlog.Infof(msg)\n}\n\nfunc inspect(i interface{}) (s string) {\n\tval := reflect.ValueOf(i)\n\telem := val.Type().Elem()\n\n\teName := elem.String()\n\tfor i := 0; i < elem.NumField(); i++ {\n\t\tfield := elem.Field(i)\n\t\tfieldVal := reflect.Indirect(val).FieldByName(field.Name)\n\t\ts += fmt.Sprintf(\"%s.%s = \", eName, field.Name)\n\t\ts += fmt.Sprintf(\"%v (%s)\\n\", fieldVal, field.Type)\n\t}\n\treturn s\n}\n\nfunc quote(s string) string {\n\treturn fmt.Sprintf(\"\\\"%s\\\"\", s)\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/bcicen/ctop\n\nrequire (\n\tgithub.com/BurntSushi/toml v0.3.1\n\tgithub.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd\n\tgithub.com/fsouza/go-dockerclient v1.7.0\n\tgithub.com/gizak/termui v2.3.1-0.20180817033724-8d4faad06196+incompatible\n\tgithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect\n\tgithub.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c\n\tgithub.com/mattn/go-runewidth v0.0.2\n\tgithub.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d\n\tgithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d\n\tgithub.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473\n\tgithub.com/opencontainers/runc v1.1.0\n\tgithub.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/stretchr/testify v1.4.0\n)\n\nrequire (\n\tgithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect\n\tgithub.com/Microsoft/go-winio v0.4.16 // indirect\n\tgithub.com/Microsoft/hcsshim v0.8.10 // indirect\n\tgithub.com/checkpoint-restore/go-criu/v5 v5.3.0 // indirect\n\tgithub.com/cilium/ebpf v0.7.0 // indirect\n\tgithub.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 // indirect\n\tgithub.com/containerd/console v1.0.3 // indirect\n\tgithub.com/containerd/containerd v1.4.1 // indirect\n\tgithub.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.2.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible // indirect\n\tgithub.com/docker/go-connections v0.4.0 // indirect\n\tgithub.com/docker/go-units v0.4.0 // indirect\n\tgithub.com/godbus/dbus/v5 v5.0.6 // indirect\n\tgithub.com/gogo/protobuf v1.3.1 // indirect\n\tgithub.com/hashicorp/golang-lru v0.5.1 // indirect\n\tgithub.com/maruel/panicparse v1.6.1 // indirect\n\tgithub.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect\n\tgithub.com/moby/sys/mount v0.2.0 // indirect\n\tgithub.com/moby/sys/mountinfo v0.5.0 // indirect\n\tgithub.com/moby/term v0.0.0-20201110203204-bea5bbe245bf // indirect\n\tgithub.com/morikuni/aec v1.0.0 // indirect\n\tgithub.com/mrunalp/fileutils v0.5.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.0.1 // indirect\n\tgithub.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect\n\tgithub.com/opencontainers/selinux v1.10.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 // indirect\n\tgithub.com/sirupsen/logrus v1.8.1 // indirect\n\tgithub.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect\n\tgithub.com/vishvananda/netlink v1.1.0 // indirect\n\tgithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect\n\tgo.opencensus.io v0.22.0 // indirect\n\tgolang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect\n\tgolang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect\n\tgolang.org/x/sys v0.0.0-20211116061358-0a5406a5449c // indirect\n\tgoogle.golang.org/protobuf v1.27.1 // indirect\n\tgopkg.in/yaml.v2 v2.2.8 // indirect\n)\n\ngo 1.18\n"
  },
  {
    "path": "go.sum",
    "content": "bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=\ngithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=\ngithub.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=\ngithub.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=\ngithub.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU=\ngithub.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM=\ngithub.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd h1:xqaBnULC8wEnQpRDXAsDgXkU/STqoluz1REOoegSfNU=\ngithub.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=\ngithub.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8=\ngithub.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=\ngithub.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=\ngithub.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k=\ngithub.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk=\ngithub.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=\ngithub.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=\ngithub.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=\ngithub.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a h1:jEIoR0aA5GogXZ8pP3DUzE+zrhaF6/1rYZy+7KkYEWM=\ngithub.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=\ngithub.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=\ngithub.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=\ngithub.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=\ngithub.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=\ngithub.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=\ngithub.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=\ngithub.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible h1:lwpV3629md5omgAKjxPWX17shI7vMRpE3nyb9WHn8pA=\ngithub.com/docker/docker v20.10.0-beta1.0.20201113105859-b6bfff2a628f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/fsouza/go-dockerclient v1.7.0 h1:Ie1/8pAnBHNyCbSIDnYKBdXUEobk4AeJhWZz7k6rWfc=\ngithub.com/fsouza/go-dockerclient v1.7.0/go.mod h1:Ny0LfP7OOsYu9nAi4339E4Ifor6nGBFO2M8lnd2nR+c=\ngithub.com/gizak/termui v2.3.1-0.20180817033724-8d4faad06196+incompatible h1:pUbrySwhNIu18YXjMTCt/Z3kr8eYQ8hRDs4BeR/crmA=\ngithub.com/gizak/termui v2.3.1-0.20180817033724-8d4faad06196+incompatible/go.mod h1:PkJoWUt/zacQKysNfQtcw1RW+eK2SxkieVBtl+4ovLA=\ngithub.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=\ngithub.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=\ngithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=\ngithub.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c h1:/hc+TxW4Q1v6aqNPHE5jiaNF2xEK0CzWTgo25RQhQ+U=\ngithub.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.mod h1:FJRkXmPrkHw0WDjB/LXMUhjWJ112Y6JUYnIVBOy8oH8=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/maruel/panicparse v1.6.1 h1:803MjBzGcUgE1vYgg3UMNq3G1oyYeKkMu3t6hBS97x0=\ngithub.com/maruel/panicparse v1.6.1/go.mod h1:uoxI4w9gJL6XahaYPMq/z9uadrdr1SyHuQwV2q80Mm0=\ngithub.com/maruel/panicparse/v2 v2.1.1/go.mod h1:AeTWdCE4lcq8OKsLb6cHSj1RWHVSnV9HBCk7sKLF4Jg=\ngithub.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=\ngithub.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM=\ngithub.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM=\ngithub.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=\ngithub.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI=\ngithub.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=\ngithub.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyPiK2mtZnPrpDl5UnZ64eCkw=\ngithub.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=\ngithub.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=\ngithub.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=\ngithub.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=\ngithub.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=\ngithub.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=\ngithub.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.1.0 h1:O9+X96OcDjkmmZyfaG996kV7yq8HsoU2h1XRRQcefG8=\ngithub.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=\ngithub.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc=\ngithub.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/selinux v1.10.0 h1:rAiKF8hTcgLI3w0DHm6i0ylVVcOrlgR1kK99DRLDhyU=\ngithub.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=\ngithub.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 h1:dofHuld+js7eKSemxqTVIo8yRlpRw+H1SdpzZxWruBc=\ngithub.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 h1:58EBmR2dMNL2n/FnbQewK3D14nXr0V9CObDSvMJLq+Y=\ngithub.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=\ngithub.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=\ngithub.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=\ngo.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211116061358-0a5406a5449c h1:DHcbWVXeY+0Y8HHKR+rbLwnoh2F4tNCY7rTiHJ30RmA=\ngolang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\ngotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "grid.go",
    "content": "package main\n\nimport (\n\t\"github.com/bcicen/ctop/config\"\n\t\"github.com/bcicen/ctop/cwidgets/single\"\n\tui \"github.com/gizak/termui\"\n)\n\nfunc ShowConnError(err error) (exit bool) {\n\tui.Clear()\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tsetErr := func(err error) {\n\t\terrView.Append(err.Error())\n\t\terrView.Append(\"attempting to reconnect...\")\n\t\tui.Render(errView)\n\t}\n\n\tHandleKeys(\"exit\", func() {\n\t\texit = true\n\t\tui.StopLoop()\n\t})\n\n\tui.Handle(\"/timer/1s\", func(ui.Event) {\n\t\t_, err := cursor.RefreshContainers()\n\t\tif err == nil {\n\t\t\tui.StopLoop()\n\t\t\treturn\n\t\t}\n\t\tsetErr(err)\n\t})\n\n\tui.Handle(\"/sys/wnd/resize\", func(e ui.Event) {\n\t\terrView.Resize()\n\t\tui.Clear()\n\t\tui.Render(errView)\n\t\tlog.Infof(\"RESIZE\")\n\t})\n\n\terrView.Resize()\n\tsetErr(err)\n\tui.Loop()\n\treturn exit\n}\n\nfunc RedrawRows(clr bool) {\n\t// reinit body rows\n\tcGrid.Clear()\n\n\t// build layout\n\ty := 1\n\tif config.GetSwitchVal(\"enableHeader\") {\n\t\theader.SetCount(cursor.Len())\n\t\theader.SetFilter(config.GetVal(\"filterStr\"))\n\t\ty += header.Height()\n\t}\n\n\tcGrid.SetY(y)\n\n\tfor _, c := range cursor.filtered {\n\t\tcGrid.AddRows(c.Widgets)\n\t}\n\n\tif clr {\n\t\tui.Clear()\n\t\tlog.Debugf(\"screen cleared\")\n\t}\n\tif config.GetSwitchVal(\"enableHeader\") {\n\t\tui.Render(header)\n\t}\n\tcGrid.Align()\n\tui.Render(cGrid)\n}\n\nfunc SingleView() MenuFn {\n\tc := cursor.Selected()\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tui.Clear()\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tex := single.NewSingle()\n\tc.SetUpdater(ex)\n\n\tex.Align()\n\tui.Render(ex)\n\n\tHandleKeys(\"up\", ex.Up)\n\tHandleKeys(\"down\", ex.Down)\n\tui.Handle(\"/sys/kbd/\", func(ui.Event) { ui.StopLoop() })\n\n\tui.Handle(\"/timer/1s\", func(ui.Event) { ui.Render(ex) })\n\tui.Handle(\"/sys/wnd/resize\", func(e ui.Event) {\n\t\tex.SetWidth(ui.TermWidth())\n\t\tex.Align()\n\t\tlog.Infof(\"resize: width=%v max-rows=%v\", ex.Width, cGrid.MaxRows())\n\t})\n\n\tui.Loop()\n\tc.SetUpdater(c.Widgets)\n\treturn nil\n}\n\nfunc RefreshDisplay() error {\n\t// skip display refresh during scroll\n\tif !cursor.isScrolling {\n\t\tneedsClear, err := cursor.RefreshContainers()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tRedrawRows(needsClear)\n\t}\n\treturn nil\n}\n\nfunc Display() bool {\n\tvar menu MenuFn\n\tvar connErr error\n\n\tcGrid.SetWidth(ui.TermWidth())\n\tui.DefaultEvtStream.Hook(logEvent)\n\n\t// initial draw\n\theader.Align()\n\tstatus.Align()\n\tcursor.RefreshContainers()\n\tRedrawRows(true)\n\n\tHandleKeys(\"up\", cursor.Up)\n\tHandleKeys(\"down\", cursor.Down)\n\n\tHandleKeys(\"pgup\", cursor.PgUp)\n\tHandleKeys(\"pgdown\", cursor.PgDown)\n\n\tHandleKeys(\"exit\", ui.StopLoop)\n\tHandleKeys(\"help\", func() {\n\t\tmenu = HelpMenu\n\t\tui.StopLoop()\n\t})\n\n\tui.Handle(\"/sys/kbd/<enter>\", func(ui.Event) {\n\t\tmenu = ContainerMenu\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/<left>\", func(ui.Event) {\n\t\tmenu = LogMenu\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/<right>\", func(ui.Event) {\n\t\tmenu = SingleView\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/l\", func(ui.Event) {\n\t\tmenu = LogMenu\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/e\", func(ui.Event) {\n\t\tmenu = ExecShell\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/w\", func(ui.Event) {\n\t\tmenu = OpenInBrowser()\n\t})\n\tui.Handle(\"/sys/kbd/o\", func(ui.Event) {\n\t\tmenu = SingleView\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/a\", func(ui.Event) {\n\t\tconfig.Toggle(\"allContainers\")\n\t\tconnErr = RefreshDisplay()\n\t\tif connErr != nil {\n\t\t\tui.StopLoop()\n\t\t}\n\t})\n\tui.Handle(\"/sys/kbd/D\", func(ui.Event) {\n\t\tdumpContainer(cursor.Selected())\n\t})\n\tui.Handle(\"/sys/kbd/f\", func(ui.Event) {\n\t\tmenu = FilterMenu\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/H\", func(ui.Event) {\n\t\tconfig.Toggle(\"enableHeader\")\n\t\tRedrawRows(true)\n\t})\n\tui.Handle(\"/sys/kbd/r\", func(e ui.Event) {\n\t\tconfig.Toggle(\"sortReversed\")\n\t})\n\tui.Handle(\"/sys/kbd/s\", func(ui.Event) {\n\t\tmenu = SortMenu\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/c\", func(ui.Event) {\n\t\tmenu = ColumnsMenu\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/S\", func(ui.Event) {\n\t\tpath, err := config.Write()\n\t\tif err == nil {\n\t\t\tlog.Statusf(\"wrote config to %s\", path)\n\t\t} else {\n\t\t\tlog.StatusErr(err)\n\t\t}\n\t\tui.StopLoop()\n\t})\n\n\tui.Handle(\"/timer/1s\", func(e ui.Event) {\n\t\tif log.StatusQueued() {\n\t\t\tui.StopLoop()\n\t\t}\n\t\tconnErr = RefreshDisplay()\n\t\tif connErr != nil {\n\t\t\tui.StopLoop()\n\t\t}\n\t})\n\n\tui.Handle(\"/sys/wnd/resize\", func(e ui.Event) {\n\t\theader.Align()\n\t\tstatus.Align()\n\t\tcursor.ScrollPage()\n\t\tcGrid.SetWidth(ui.TermWidth())\n\t\tlog.Infof(\"resize: width=%v max-rows=%v\", cGrid.Width, cGrid.MaxRows())\n\t\tRedrawRows(true)\n\t})\n\n\tui.Loop()\n\n\tif connErr != nil {\n\t\treturn ShowConnError(connErr)\n\t}\n\n\tif log.StatusQueued() {\n\t\tfor sm := range log.FlushStatus() {\n\t\t\tif sm.IsError {\n\t\t\t\tstatus.ShowErr(sm.Text)\n\t\t\t} else {\n\t\t\t\tstatus.Show(sm.Text)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\tif menu != nil {\n\t\tfor menu != nil {\n\t\t\tmenu = menu()\n\t\t}\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "install.sh",
    "content": "#!/usr/bin/env bash\n# a simple install script for ctop\n\nKERNEL=$(uname -s)\n\nfunction output() { echo -e \"\\033[32mctop-install\\033[0m $@\"; }\n\nfunction command_exists() {\n  command -v \"$@\" > /dev/null 2>&1\n}\n\n# extract github download url matching pattern\nfunction extract_url() {\n  match=$1; shift\n  echo \"$@\" | while read line; do\n    case $line in\n      *browser_download_url*${match}*)\n        url=$(echo $line | sed -e 's/^.*\"browser_download_url\":[ ]*\"//' -e 's/\".*//;s/\\ //g')\n        echo $url\n        break\n      ;;\n    esac\n  done\n}\n\ncase $KERNEL in\n  Linux) MATCH_BUILD=\"linux-amd64\" ;;\n  Darwin) MATCH_BUILD=\"darwin-amd64\" ;;\n  *)\n    echo \"platform not supported by this install script\"\n    exit 1\n    ;;\nesac\n\nfor req in curl wget; do\n  command_exists $req || {\n    output \"missing required $req binary\"\n    req_failed=1\n  }\ndone\n[ \"$req_failed\" == 1 ] && exit 1\n\nsh_c='sh -c'\nif [ \"$CURRENT_USER\" != 'root' ]; then\n  if command_exists sudo; then\n    sh_c='sudo -E sh -c'\n  elif command_exists su; then\n    sh_c='su -c'\n  else\n    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.\"\n    exit 1\n  fi\nfi\n\nTMP=$(mktemp -d \"${TMPDIR:-/tmp}/ctop.XXXXX\")\ncd ${TMP}\n\noutput \"fetching latest release info\"\nresp=$(curl -s https://api.github.com/repos/bcicen/ctop/releases/latest)\n\noutput \"fetching release checksums\"\nchecksum_url=$(extract_url sha256sums.txt \"$resp\")\nwget -q $checksum_url -O sha256sums.txt\n\n# skip if latest already installed\ncur_ctop=$(which ctop 2> /dev/null)\nif [[ -n \"$cur_ctop\" ]]; then\n  cur_sum=$(sha256sum $cur_ctop | sed 's/ .*//')\n  (grep -q $cur_sum sha256sums.txt) && {\n    output \"already up-to-date\"\n    exit 0\n  }\nfi\n\noutput \"fetching latest ctop\"\nurl=$(extract_url $MATCH_BUILD \"$resp\")\nwget -q --show-progress $url\n(sha256sum -c --quiet --ignore-missing sha256sums.txt) || exit 1\n\noutput \"installing to /usr/local/bin\"\nchmod +x ctop-*\n$sh_c \"mv ctop-* /usr/local/bin/ctop\"\n\noutput \"done!\"\n"
  },
  {
    "path": "keys.go",
    "content": "package main\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\n// Common action keybindings\nvar keyMap = map[string][]string{\n\t\"up\": []string{\n\t\t\"/sys/kbd/<up>\",\n\t\t\"/sys/kbd/k\",\n\t},\n\t\"down\": []string{\n\t\t\"/sys/kbd/<down>\",\n\t\t\"/sys/kbd/j\",\n\t},\n\t\"pgup\": []string{\n\t\t\"/sys/kbd/<previous>\",\n\t\t\"/sys/kbd/C-<up>\",\n\t},\n\t\"pgdown\": []string{\n\t\t\"/sys/kbd/<next>\",\n\t\t\"/sys/kbd/C-<down>\",\n\t},\n\t\"exit\": []string{\n\t\t\"/sys/kbd/q\",\n\t\t\"/sys/kbd/C-c\",\n\t\t\"/sys/kbd/<escape>\",\n\t},\n\t\"help\": []string{\n\t\t\"/sys/kbd/h\",\n\t\t\"/sys/kbd/?\",\n\t},\n}\n\n// Apply a common handler function to all given keys\nfunc HandleKeys(i string, f func()) {\n\tfor _, k := range keyMap[i] {\n\t\tui.Handle(k, func(ui.Event) { f() })\n\t}\n}\n"
  },
  {
    "path": "logging/main.go",
    "content": "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    *CTopLogger\n\texited bool\n\tlevel  = logging.INFO // default level\n\tformat = logging.MustStringFormatter(\n\t\t`%{color}%{time:15:04:05.000} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,\n\t)\n)\n\ntype statusMsg struct {\n\tText    string\n\tIsError bool\n}\n\ntype CTopLogger struct {\n\t*logging.Logger\n\tbackend *logging.MemoryBackend\n\tlogFile *os.File\n\tsLog    []statusMsg\n}\n\nfunc (c *CTopLogger) FlushStatus() chan statusMsg {\n\tch := make(chan statusMsg)\n\tgo func() {\n\t\tfor _, sm := range c.sLog {\n\t\t\tch <- sm\n\t\t}\n\t\tclose(ch)\n\t\tc.sLog = []statusMsg{}\n\t}()\n\treturn ch\n}\n\nfunc (c *CTopLogger) StatusQueued() bool     { return len(c.sLog) > 0 }\nfunc (c *CTopLogger) Status(s string)        { c.addStatus(statusMsg{s, false}) }\nfunc (c *CTopLogger) StatusErr(err error)    { c.addStatus(statusMsg{err.Error(), true}) }\nfunc (c *CTopLogger) addStatus(sm statusMsg) { c.sLog = append(c.sLog, sm) }\n\nfunc (c *CTopLogger) Statusf(s string, a ...interface{}) { c.Status(fmt.Sprintf(s, a...)) }\n\nfunc Init() *CTopLogger {\n\tif Log == nil {\n\t\tlogging.SetFormatter(format) // setup default formatter\n\n\t\tLog = &CTopLogger{\n\t\t\tlogging.MustGetLogger(\"ctop\"),\n\t\t\tlogging.NewMemoryBackend(size),\n\t\t\tnil,\n\t\t\t[]statusMsg{},\n\t\t}\n\n\t\tdebugMode := debugMode()\n\t\tif debugMode {\n\t\t\tlevel = logging.DEBUG\n\t\t}\n\t\tbackendLvl := logging.AddModuleLevel(Log.backend)\n\t\tbackendLvl.SetLevel(level, \"\")\n\n\t\tlogFilePath := debugModeFile()\n\t\tif logFilePath == \"\" {\n\t\t\tlogging.SetBackend(backendLvl)\n\t\t} else {\n\t\t\tlogFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)\n\t\t\tif err != nil {\n\t\t\t\tlogging.SetBackend(backendLvl)\n\t\t\t\tLog.Error(\"Unable to create log file: %s\", err.Error())\n\t\t\t} else {\n\t\t\t\tbackendFile := logging.NewLogBackend(logFile, \"\", 0)\n\t\t\t\tbackendFileLvl := logging.AddModuleLevel(backendFile)\n\t\t\t\tbackendFileLvl.SetLevel(level, \"\")\n\t\t\t\tlogging.SetBackend(backendLvl, backendFileLvl)\n\t\t\t\tLog.logFile = logFile\n\t\t\t}\n\t\t}\n\n\t\tif debugMode {\n\t\t\tStartServer()\n\t\t}\n\t\tLog.Notice(\"logger initialized\")\n\t}\n\treturn Log\n}\n\nfunc (log *CTopLogger) tail() chan string {\n\tstream := make(chan string)\n\n\tnode := log.backend.Head()\n\tgo func() {\n\t\tfor {\n\t\t\tstream <- node.Record.Formatted(0)\n\t\t\tfor {\n\t\t\t\tnnode := node.Next()\n\t\t\t\tif nnode != nil {\n\t\t\t\t\tnode = nnode\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif exited {\n\t\t\t\t\tclose(stream)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn stream\n}\n\nfunc (log *CTopLogger) Exit() {\n\texited = true\n\tif log.logFile != nil {\n\t\t_ = log.logFile.Close()\n\t}\n\tStopServer()\n}\n\nfunc debugMode() bool       { return os.Getenv(\"CTOP_DEBUG\") == \"1\" }\nfunc debugModeTCP() bool    { return os.Getenv(\"CTOP_DEBUG_TCP\") == \"1\" }\nfunc debugModeFile() string { return os.Getenv(\"CTOP_DEBUG_FILE\") }\n"
  },
  {
    "path": "logging/server.go",
    "content": "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\"\n)\n\nvar server struct {\n\twg sync.WaitGroup\n\tln net.Listener\n}\n\nfunc getListener() net.Listener {\n\tvar ln net.Listener\n\tvar err error\n\tif debugModeTCP() {\n\t\tln, err = net.Listen(\"tcp\", socketAddr)\n\t} else {\n\t\tln, err = net.Listen(\"unix\", socketPath)\n\t}\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn ln\n}\n\nfunc StartServer() {\n\tserver.ln = getListener()\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := server.ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\tif err, ok := err.(net.Error); ok && err.Temporary() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgo handler(conn)\n\t\t}\n\t}()\n\n\tLog.Notice(\"logging server started\")\n}\n\nfunc StopServer() {\n\tserver.wg.Wait()\n\tif server.ln != nil {\n\t\tserver.ln.Close()\n\t}\n}\n\nfunc handler(wc io.WriteCloser) {\n\tserver.wg.Add(1)\n\tdefer server.wg.Done()\n\tdefer wc.Close()\n\tfor msg := range Log.tail() {\n\t\tmsg = fmt.Sprintf(\"%s\\n\", msg)\n\t\twc.Write([]byte(msg))\n\t}\n\twc.Write([]byte(\"bye\\n\"))\n}\n"
  },
  {
    "path": "main.go",
    "content": "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/ctop/connector\"\n\t\"github.com/bcicen/ctop/container\"\n\t\"github.com/bcicen/ctop/cwidgets/compact\"\n\t\"github.com/bcicen/ctop/logging\"\n\t\"github.com/bcicen/ctop/widgets\"\n\tui \"github.com/gizak/termui\"\n\ttm \"github.com/nsf/termbox-go\"\n)\n\nvar (\n\tbuild     = \"none\"\n\tversion   = \"dev-build\"\n\tgoVersion = runtime.Version()\n\n\tlog     *logging.CTopLogger\n\tcursor  *GridCursor\n\tcGrid   *compact.CompactGrid\n\theader  *widgets.CTopHeader\n\tstatus  *widgets.StatusLine\n\terrView *widgets.ErrorView\n\n\tversionStr = fmt.Sprintf(\"ctop version %v, build %v %v\", version, build, goVersion)\n)\n\nfunc main() {\n\tdefer panicExit()\n\n\t// parse command line arguments\n\tvar (\n\t\tversionFlag     = flag.Bool(\"v\", false, \"output version information and exit\")\n\t\thelpFlag        = flag.Bool(\"h\", false, \"display this help dialog\")\n\t\tfilterFlag      = flag.String(\"f\", \"\", \"filter containers\")\n\t\tactiveOnlyFlag  = flag.Bool(\"a\", false, \"show active containers only\")\n\t\tsortFieldFlag   = flag.String(\"s\", \"\", \"select container sort field\")\n\t\treverseSortFlag = flag.Bool(\"r\", false, \"reverse container sort order\")\n\t\tinvertFlag      = flag.Bool(\"i\", false, \"invert default colors\")\n\t\tconnectorFlag   = flag.String(\"connector\", \"docker\", \"container connector to use\")\n\t)\n\tflag.Parse()\n\n\tif *versionFlag {\n\t\tfmt.Println(versionStr)\n\t\tos.Exit(0)\n\t}\n\n\tif *helpFlag {\n\t\tprintHelp()\n\t\tos.Exit(0)\n\t}\n\n\t// init logger\n\tlog = logging.Init()\n\n\t// init global config and read config file if exists\n\tconfig.Init()\n\tif err := config.Read(); err != nil {\n\t\tlog.Warningf(\"reading config: %s\", err)\n\t}\n\n\t// override default config values with command line flags\n\tif *filterFlag != \"\" {\n\t\tconfig.Update(\"filterStr\", *filterFlag)\n\t}\n\n\tif *activeOnlyFlag {\n\t\tconfig.Toggle(\"allContainers\")\n\t}\n\n\tif *sortFieldFlag != \"\" {\n\t\tvalidSort(*sortFieldFlag)\n\t\tconfig.Update(\"sortField\", *sortFieldFlag)\n\t}\n\n\tif *reverseSortFlag {\n\t\tconfig.Toggle(\"sortReversed\")\n\t}\n\n\t// init ui\n\tif *invertFlag {\n\t\tInvertColorMap()\n\t}\n\tui.ColorMap = ColorMap // override default colormap\n\tif err := ui.Init(); err != nil {\n\t\tpanic(err)\n\t}\n\ttm.SetInputMode(tm.InputAlt)\n\n\tdefer Shutdown()\n\t// init grid, cursor, header\n\tcSuper, err := connector.ByName(*connectorFlag)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcursor = &GridCursor{cSuper: cSuper}\n\tcGrid = compact.NewCompactGrid()\n\theader = widgets.NewCTopHeader()\n\tstatus = widgets.NewStatusLine()\n\terrView = widgets.NewErrorView()\n\n\tfor {\n\t\texit := Display()\n\t\tif exit {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc Shutdown() {\n\tlog.Notice(\"shutting down\")\n\tlog.Exit()\n\tif tm.IsInit {\n\t\tui.Close()\n\t}\n}\n\n// ensure a given sort field is valid\nfunc validSort(s string) {\n\tif _, ok := container.Sorters[s]; !ok {\n\t\tfmt.Printf(\"invalid sort field: %s\\n\", s)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc panicExit() {\n\tif r := recover(); r != nil {\n\t\tShutdown()\n\t\tpanic(r)\n\t\tfmt.Printf(\"error: %s\\n\", r)\n\t\tos.Exit(1)\n\t}\n}\n\nvar helpMsg = `ctop - interactive container viewer\n\nusage: ctop [options]\n\noptions:\n`\n\nfunc printHelp() {\n\tfmt.Println(helpMsg)\n\tflag.PrintDefaults()\n\tfmt.Printf(\"\\navailable connectors: \")\n\tfmt.Println(strings.Join(connector.Enabled(), \", \"))\n}\n"
  },
  {
    "path": "menus.go",
    "content": "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\"github.com/bcicen/ctop/widgets\"\n\t\"github.com/bcicen/ctop/widgets/menu\"\n\tui \"github.com/gizak/termui\"\n\t\"github.com/pkg/browser\"\n)\n\n// MenuFn executes a menu window, returning the next menu or nil\ntype MenuFn func() MenuFn\n\nvar helpDialog = []menu.Item{\n\t{\"<enter> - open container menu\", \"\"},\n\t{\"\", \"\"},\n\t{\"[a] - toggle display of all containers\", \"\"},\n\t{\"[f] - filter displayed containers\", \"\"},\n\t{\"[h] - open this help dialog\", \"\"},\n\t{\"[H] - toggle ctop header\", \"\"},\n\t{\"[s] - select container sort field\", \"\"},\n\t{\"[r] - reverse container sort order\", \"\"},\n\t{\"[o] - open single view\", \"\"},\n\t{\"[l] - view container logs ([t] to toggle timestamp when open)\", \"\"},\n\t{\"[e] - exec shell\", \"\"},\n\t{\"[w] - open browser (first port is http)\", \"\"},\n\t{\"[c] - configure columns\", \"\"},\n\t{\"[S] - save current configuration to file\", \"\"},\n\t{\"[q] - exit ctop\", \"\"},\n}\n\nfunc HelpMenu() MenuFn {\n\tui.Clear()\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tm := menu.NewMenu()\n\tm.BorderLabel = \"Help\"\n\tm.AddItems(helpDialog...)\n\tui.Handle(\"/sys/wnd/resize\", func(e ui.Event) {\n\t\tui.Clear()\n\t\tui.Render(m)\n\t})\n\tui.Handle(\"/sys/kbd/\", func(ui.Event) {\n\t\tui.StopLoop()\n\t})\n\tui.Loop()\n\treturn nil\n}\n\nfunc FilterMenu() MenuFn {\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\ti := widgets.NewInput()\n\ti.BorderLabel = \"Filter\"\n\ti.SetY(ui.TermHeight() - i.Height)\n\ti.Data = config.GetVal(\"filterStr\")\n\tui.Render(i)\n\n\t// refresh container rows on input\n\tstream := i.Stream()\n\tgo func() {\n\t\tfor s := range stream {\n\t\t\tconfig.Update(\"filterStr\", s)\n\t\t\tRefreshDisplay()\n\t\t\tui.Render(i)\n\t\t}\n\t}()\n\n\ti.InputHandlers()\n\tui.Handle(\"/sys/kbd/<escape>\", func(ui.Event) {\n\t\tconfig.Update(\"filterStr\", \"\")\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/<enter>\", func(ui.Event) {\n\t\tconfig.Update(\"filterStr\", i.Data)\n\t\tui.StopLoop()\n\t})\n\tui.Loop()\n\treturn nil\n}\n\nfunc SortMenu() MenuFn {\n\tui.Clear()\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tm := menu.NewMenu()\n\tm.Selectable = true\n\tm.SortItems = true\n\tm.BorderLabel = \"Sort Field\"\n\n\tfor _, field := range container.SortFields() {\n\t\tm.AddItems(menu.Item{field, \"\"})\n\t}\n\n\t// set cursor position to current sort field\n\tm.SetCursor(config.GetVal(\"sortField\"))\n\n\tHandleKeys(\"up\", m.Up)\n\tHandleKeys(\"down\", m.Down)\n\tHandleKeys(\"exit\", ui.StopLoop)\n\n\tui.Handle(\"/sys/kbd/<enter>\", func(ui.Event) {\n\t\tconfig.Update(\"sortField\", m.SelectedValue())\n\t\tui.StopLoop()\n\t})\n\n\tui.Render(m)\n\tui.Loop()\n\treturn nil\n}\n\nfunc ColumnsMenu() MenuFn {\n\tconst (\n\t\tenabledStr  = \"[X]\"\n\t\tdisabledStr = \"[ ]\"\n\t\tpadding     = 2\n\t)\n\n\tui.Clear()\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tm := menu.NewMenu()\n\tm.Selectable = true\n\tm.SortItems = false\n\tm.BorderLabel = \"Columns\"\n\tm.SubText = \"Re-order: <Page Up> / <Page Down>\"\n\n\trebuild := func() {\n\t\t// get padding for right alignment of enabled status\n\t\tvar maxLen int\n\t\tfor _, col := range config.GlobalColumns {\n\t\t\tif len(col.Label) > maxLen {\n\t\t\t\tmaxLen = len(col.Label)\n\t\t\t}\n\t\t}\n\t\tmaxLen += padding\n\n\t\t// rebuild menu items\n\t\tm.ClearItems()\n\t\tfor _, col := range config.GlobalColumns {\n\t\t\ttxt := col.Label + strings.Repeat(\" \", maxLen-len(col.Label))\n\t\t\tif col.Enabled {\n\t\t\t\ttxt += enabledStr\n\t\t\t} else {\n\t\t\t\ttxt += disabledStr\n\t\t\t}\n\t\t\tm.AddItems(menu.Item{col.Name, txt})\n\t\t}\n\t}\n\n\tupFn := func() {\n\t\tconfig.ColumnLeft(m.SelectedValue())\n\t\tm.Up()\n\t\trebuild()\n\t}\n\n\tdownFn := func() {\n\t\tconfig.ColumnRight(m.SelectedValue())\n\t\tm.Down()\n\t\trebuild()\n\t}\n\n\ttoggleFn := func() {\n\t\tconfig.ColumnToggle(m.SelectedValue())\n\t\trebuild()\n\t}\n\n\trebuild()\n\n\tHandleKeys(\"up\", m.Up)\n\tHandleKeys(\"down\", m.Down)\n\tHandleKeys(\"enter\", toggleFn)\n\tHandleKeys(\"pgup\", upFn)\n\tHandleKeys(\"pgdown\", downFn)\n\n\tui.Handle(\"/sys/kbd/x\", func(ui.Event) { toggleFn() })\n\tui.Handle(\"/sys/kbd/<enter>\", func(ui.Event) { toggleFn() })\n\n\tHandleKeys(\"exit\", func() {\n\t\tcSource, err := cursor.cSuper.Get()\n\t\tif err == nil {\n\t\t\tfor _, c := range cSource.All() {\n\t\t\t\tc.RecreateWidgets()\n\t\t\t}\n\t\t}\n\t\tui.StopLoop()\n\t})\n\n\tui.Render(m)\n\tui.Loop()\n\treturn nil\n}\n\nfunc ContainerMenu() MenuFn {\n\tc := cursor.Selected()\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tm := menu.NewMenu()\n\tm.Selectable = true\n\tm.BorderLabel = \"Menu\"\n\n\titems := []menu.Item{\n\t\tmenu.Item{Val: \"single\", Label: \"[o] single view\"},\n\t\tmenu.Item{Val: \"logs\", Label: \"[l] log view\"},\n\t}\n\n\tif c.Meta[\"state\"] == \"running\" {\n\t\titems = append(items, menu.Item{Val: \"stop\", Label: \"[s] stop\"})\n\t\titems = append(items, menu.Item{Val: \"pause\", Label: \"[p] pause\"})\n\t\titems = append(items, menu.Item{Val: \"restart\", Label: \"[r] restart\"})\n\t\titems = append(items, menu.Item{Val: \"exec\", Label: \"[e] exec shell\"})\n\t\tif c.Meta[\"Web Port\"] != \"\" {\n\t\t\titems = append(items, menu.Item{Val: \"browser\", Label: \"[w] open in browser\"})\n\t\t}\n\t}\n\tif c.Meta[\"state\"] == \"exited\" || c.Meta[\"state\"] == \"created\" {\n\t\titems = append(items, menu.Item{Val: \"start\", Label: \"[s] start\"})\n\t\titems = append(items, menu.Item{Val: \"remove\", Label: \"[R] remove\"})\n\t}\n\tif c.Meta[\"state\"] == \"paused\" {\n\t\titems = append(items, menu.Item{Val: \"unpause\", Label: \"[p] unpause\"})\n\t}\n\titems = append(items, menu.Item{Val: \"cancel\", Label: \"[c] cancel\"})\n\n\tm.AddItems(items...)\n\tui.Render(m)\n\n\tHandleKeys(\"up\", m.Up)\n\tHandleKeys(\"down\", m.Down)\n\n\tvar selected string\n\n\t// shortcuts\n\tui.Handle(\"/sys/kbd/o\", func(ui.Event) {\n\t\tselected = \"single\"\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/l\", func(ui.Event) {\n\t\tselected = \"logs\"\n\t\tui.StopLoop()\n\t})\n\tif c.Meta[\"state\"] != \"paused\" {\n\t\tui.Handle(\"/sys/kbd/s\", func(ui.Event) {\n\t\t\tif c.Meta[\"state\"] == \"running\" {\n\t\t\t\tselected = \"stop\"\n\t\t\t} else {\n\t\t\t\tselected = \"start\"\n\t\t\t}\n\t\t\tui.StopLoop()\n\t\t})\n\t}\n\tif c.Meta[\"state\"] != \"exited\" && c.Meta[\"state\"] != \"created\" {\n\t\tui.Handle(\"/sys/kbd/p\", func(ui.Event) {\n\t\t\tif c.Meta[\"state\"] == \"paused\" {\n\t\t\t\tselected = \"unpause\"\n\t\t\t} else {\n\t\t\t\tselected = \"pause\"\n\t\t\t}\n\t\t\tui.StopLoop()\n\t\t})\n\t}\n\tif c.Meta[\"state\"] == \"running\" {\n\t\tui.Handle(\"/sys/kbd/e\", func(ui.Event) {\n\t\t\tselected = \"exec\"\n\t\t\tui.StopLoop()\n\t\t})\n\t\tui.Handle(\"/sys/kbd/r\", func(ui.Event) {\n\t\t\tselected = \"restart\"\n\t\t\tui.StopLoop()\n\t\t})\n\t\tif c.Meta[\"Web Port\"] != \"\" {\n\t\t\tui.Handle(\"/sys/kbd/w\", func(ui.Event) {\n\t\t\t\tselected = \"browser\"\n\t\t\t})\n\t\t}\n\t}\n\tui.Handle(\"/sys/kbd/R\", func(ui.Event) {\n\t\tselected = \"remove\"\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/c\", func(ui.Event) {\n\t\tui.StopLoop()\n\t})\n\n\tui.Handle(\"/sys/kbd/<enter>\", func(ui.Event) {\n\t\tselected = m.SelectedValue()\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/sys/kbd/\", func(ui.Event) {\n\t\tui.StopLoop()\n\t})\n\tui.Loop()\n\n\tvar nextMenu MenuFn\n\tswitch selected {\n\tcase \"single\":\n\t\tnextMenu = SingleView\n\tcase \"logs\":\n\t\tnextMenu = LogMenu\n\tcase \"exec\":\n\t\tnextMenu = ExecShell\n\tcase \"browser\":\n\t\tnextMenu = OpenInBrowser\n\tcase \"start\":\n\t\tnextMenu = Confirm(confirmTxt(\"start\", c.GetMeta(\"name\")), c.Start)\n\tcase \"stop\":\n\t\tnextMenu = Confirm(confirmTxt(\"stop\", c.GetMeta(\"name\")), c.Stop)\n\tcase \"remove\":\n\t\tnextMenu = Confirm(confirmTxt(\"remove\", c.GetMeta(\"name\")), c.Remove)\n\tcase \"pause\":\n\t\tnextMenu = Confirm(confirmTxt(\"pause\", c.GetMeta(\"name\")), c.Pause)\n\tcase \"unpause\":\n\t\tnextMenu = Confirm(confirmTxt(\"unpause\", c.GetMeta(\"name\")), c.Unpause)\n\tcase \"restart\":\n\t\tnextMenu = Confirm(confirmTxt(\"restart\", c.GetMeta(\"name\")), c.Restart)\n\t}\n\n\treturn nextMenu\n}\n\nfunc LogMenu() MenuFn {\n\n\tc := cursor.Selected()\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\tlogs, quit := logReader(c)\n\tm := widgets.NewTextView(logs)\n\tm.BorderLabel = fmt.Sprintf(\"Logs [%s]\", c.GetMeta(\"name\"))\n\tui.Render(m)\n\n\tui.Handle(\"/sys/wnd/resize\", func(e ui.Event) {\n\t\tm.Resize()\n\t})\n\tui.Handle(\"/sys/kbd/t\", func(ui.Event) {\n\t\tm.Toggle()\n\t})\n\tui.Handle(\"/sys/kbd/\", func(ui.Event) {\n\t\tquit <- true\n\t\tui.StopLoop()\n\t})\n\tui.Loop()\n\treturn nil\n}\n\nfunc ExecShell() MenuFn {\n\tc := cursor.Selected()\n\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\t// Detect and execute default shell in container.\n\t// Execute Ash shell command: /bin/sh -c\n\t// Reset colors: printf '\\e[0m\\e[?25h'\n\t// Clear screen\n\t// Run default shell for the user. It's configured in /etc/passwd and looks like root:x:0:0:root:/root:/bin/bash:\n\t//  1. Get current user id: id -un\n\t//  2. Find user's line in /etc/passwd by grep\n\t//  3. Extract default user's shell by cutting seven's column separated by :\n\t//  4. Execute the shell path with eval\n\tif 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 {\n\t\tlog.StatusErr(err)\n\t}\n\n\treturn nil\n}\n\nfunc OpenInBrowser() MenuFn {\n\tc := cursor.Selected()\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\twebPort := c.Meta.Get(\"Web Port\")\n\tif webPort == \"\" {\n\t\treturn nil\n\t}\n\tlink := \"http://\" + webPort + \"/\"\n\tbrowser.OpenURL(link)\n\treturn nil\n}\n\n// Create a confirmation dialog with a given description string and\n// func to perform if confirmed\nfunc Confirm(txt string, fn func()) MenuFn {\n\tmenu := func() MenuFn {\n\t\tui.DefaultEvtStream.ResetHandlers()\n\t\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\t\tm := menu.NewMenu()\n\t\tm.Selectable = true\n\t\tm.BorderLabel = \"Confirm\"\n\t\tm.SubText = txt\n\n\t\titems := []menu.Item{\n\t\t\tmenu.Item{Val: \"cancel\", Label: \"[c]ancel\"},\n\t\t\tmenu.Item{Val: \"yes\", Label: \"[y]es\"},\n\t\t}\n\n\t\tvar response bool\n\n\t\tm.AddItems(items...)\n\t\tui.Render(m)\n\n\t\tyes := func() {\n\t\t\tresponse = true\n\t\t\tui.StopLoop()\n\t\t}\n\n\t\tno := func() {\n\t\t\tresponse = false\n\t\t\tui.StopLoop()\n\t\t}\n\n\t\tHandleKeys(\"up\", m.Up)\n\t\tHandleKeys(\"down\", m.Down)\n\t\tHandleKeys(\"exit\", no)\n\t\tui.Handle(\"/sys/kbd/c\", func(ui.Event) { no() })\n\t\tui.Handle(\"/sys/kbd/y\", func(ui.Event) { yes() })\n\n\t\tui.Handle(\"/sys/kbd/<enter>\", func(ui.Event) {\n\t\t\tswitch m.SelectedValue() {\n\t\t\tcase \"cancel\":\n\t\t\t\tno()\n\t\t\tcase \"yes\":\n\t\t\t\tyes()\n\t\t\t}\n\t\t})\n\n\t\tui.Loop()\n\t\tif response {\n\t\t\tfn()\n\t\t}\n\t\treturn nil\n\t}\n\treturn menu\n}\n\ntype toggleLog struct {\n\ttimestamp time.Time\n\tmessage   string\n}\n\nfunc (t *toggleLog) Toggle(on bool) string {\n\tif on {\n\t\treturn fmt.Sprintf(\"%s %s\", t.timestamp.Format(\"2006-01-02T15:04:05.999Z07:00\"), t.message)\n\t}\n\treturn t.message\n}\n\nfunc logReader(container *container.Container) (logs chan widgets.ToggleText, quit chan bool) {\n\n\tlogCollector := container.Logs()\n\tstream := logCollector.Stream()\n\tlogs = make(chan widgets.ToggleText)\n\tquit = make(chan bool)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase log := <-stream:\n\t\t\t\tlogs <- &toggleLog{timestamp: log.Timestamp, message: log.Message}\n\t\t\tcase <-quit:\n\t\t\t\tlogCollector.Stop()\n\t\t\t\tclose(logs)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\treturn\n}\n\nfunc confirmTxt(a, n string) string { return fmt.Sprintf(\"%s container %s?\", a, n) }\n"
  },
  {
    "path": "models/main.go",
    "content": "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// NewMeta returns an initialized Meta map.\n// An optional series of key, values may be provided to populate the map prior to returning\nfunc NewMeta(kvs ...string) Meta {\n\tm := make(Meta)\n\n\tvar i int\n\tfor i < len(kvs)-1 {\n\t\tm[kvs[i]] = kvs[i+1]\n\t\ti += 2\n\t}\n\n\treturn m\n}\n\nfunc (m Meta) Get(k string) string {\n\tif s, ok := m[k]; ok {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\ntype Metrics struct {\n\tNCpus        uint8\n\tCPUUtil      int\n\tNetTx        int64\n\tNetRx        int64\n\tMemLimit     int64\n\tMemPercent   int\n\tMemUsage     int64\n\tIOBytesRead  int64\n\tIOBytesWrite int64\n\tPids         int\n}\n\nfunc NewMetrics() Metrics {\n\treturn Metrics{\n\t\tCPUUtil:      -1,\n\t\tNetTx:        -1,\n\t\tNetRx:        -1,\n\t\tMemUsage:     -1,\n\t\tMemPercent:   -1,\n\t\tIOBytesRead:  -1,\n\t\tIOBytesWrite: -1,\n\t\tPids:         -1,\n\t}\n}\n"
  },
  {
    "path": "widgets/error.go",
    "content": "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\tlines []string\n}\n\nfunc NewErrorView() *ErrorView {\n\tconst yPad = 1\n\tconst xPad = 2\n\n\tp := ui.NewPar(\"\")\n\tp.X = xPad\n\tp.Y = yPad\n\tp.Border = true\n\tp.Height = 10\n\tp.Width = 20\n\tp.PaddingTop = yPad\n\tp.PaddingBottom = yPad\n\tp.PaddingLeft = xPad\n\tp.PaddingRight = xPad\n\tp.BorderLabel = \" ctop - error \"\n\tp.Bg = ui.ThemeAttr(\"bg\")\n\tp.TextFgColor = ui.ThemeAttr(\"status.warn\")\n\tp.TextBgColor = ui.ThemeAttr(\"menu.text.bg\")\n\tp.BorderFg = ui.ThemeAttr(\"status.warn\")\n\tp.BorderLabelFg = ui.ThemeAttr(\"status.warn\")\n\treturn &ErrorView{p, make([]string, 0, 50)}\n}\n\nfunc (w *ErrorView) Append(s string) {\n\tif len(w.lines)+2 >= cap(w.lines) {\n\t\tw.lines = append(w.lines[:0], w.lines[2:]...)\n\t}\n\tts := time.Now().Local().Format(\"15:04:05 MST\")\n\tw.lines = append(w.lines, fmt.Sprintf(\"[%s] %s\", ts, s))\n\tw.lines = append(w.lines, \"\")\n}\n\nfunc (w *ErrorView) Buffer() ui.Buffer {\n\toffset := len(w.lines) - w.InnerHeight()\n\tif offset < 0 {\n\t\toffset = 0\n\t}\n\tw.Text = strings.Join(w.lines[offset:len(w.lines)], \"\\n\")\n\treturn w.Par.Buffer()\n}\n\nfunc (w *ErrorView) Resize() {\n\tw.Height = ui.TermHeight() - (w.PaddingTop + w.PaddingBottom)\n\tw.SetWidth(ui.TermWidth() - (w.PaddingLeft + w.PaddingRight))\n}\n"
  },
  {
    "path": "widgets/header.go",
    "content": "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\tCount  *ui.Par\n\tFilter *ui.Par\n\tbg     *ui.Par\n}\n\nfunc NewCTopHeader() *CTopHeader {\n\treturn &CTopHeader{\n\t\tTime:   headerPar(2, \"\"),\n\t\tCount:  headerPar(24, \"-\"),\n\t\tFilter: headerPar(40, \"\"),\n\t\tbg:     headerBg(),\n\t}\n}\n\nfunc (c *CTopHeader) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tc.Time.Text = timeStr()\n\tbuf.Merge(c.bg.Buffer())\n\tbuf.Merge(c.Time.Buffer())\n\tbuf.Merge(c.Count.Buffer())\n\tbuf.Merge(c.Filter.Buffer())\n\treturn buf\n}\n\nfunc (c *CTopHeader) Align() {\n\tc.bg.SetWidth(ui.TermWidth() - 1)\n}\n\nfunc (c *CTopHeader) Height() int {\n\treturn c.bg.Height\n}\n\nfunc headerBgBordered() *ui.Par {\n\tbg := ui.NewPar(\"\")\n\tbg.X = 1\n\tbg.Height = 3\n\tbg.Bg = ui.ThemeAttr(\"header.bg\")\n\treturn bg\n}\n\nfunc headerBg() *ui.Par {\n\tbg := ui.NewPar(\"\")\n\tbg.X = 1\n\tbg.Height = 1\n\tbg.Border = false\n\tbg.Bg = ui.ThemeAttr(\"header.bg\")\n\treturn bg\n}\n\nfunc (c *CTopHeader) SetCount(val int) {\n\tc.Count.Text = fmt.Sprintf(\"%d containers\", val)\n}\n\nfunc (c *CTopHeader) SetFilter(val string) {\n\tif val == \"\" {\n\t\tc.Filter.Text = \"\"\n\t} else {\n\t\tc.Filter.Text = fmt.Sprintf(\"filter: %s\", val)\n\t}\n}\n\nfunc timeStr() string {\n\tts := time.Now().Local().Format(\"15:04:05 MST\")\n\treturn fmt.Sprintf(\"ctop - %s\", ts)\n}\n\nfunc headerPar(x int, s string) *ui.Par {\n\tp := ui.NewPar(fmt.Sprintf(\" %s\", s))\n\tp.X = x\n\tp.Border = false\n\tp.Height = 1\n\tp.Width = 20\n\tp.Bg = ui.ThemeAttr(\"header.bg\")\n\tp.TextFgColor = ui.ThemeAttr(\"header.fg\")\n\tp.TextBgColor = ui.ThemeAttr(\"header.bg\")\n\treturn p\n}\n"
  },
  {
    "path": "widgets/input.go",
    "content": "package widgets\n\nimport (\n\t\"strings\"\n\n\tui \"github.com/gizak/termui\"\n)\n\nvar (\n\tinput_chars = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.\"\n)\n\ntype Padding [2]int // x,y padding\n\ntype Input struct {\n\tui.Block\n\tLabel       string\n\tData        string\n\tMaxLen      int\n\tTextFgColor ui.Attribute\n\tTextBgColor ui.Attribute\n\tstream      chan string // stream text as it changes\n\tpadding     Padding\n}\n\nfunc NewInput() *Input {\n\ti := &Input{\n\t\tBlock:       *ui.NewBlock(),\n\t\tLabel:       \"input\",\n\t\tMaxLen:      20,\n\t\tTextFgColor: ui.ThemeAttr(\"menu.text.fg\"),\n\t\tTextBgColor: ui.ThemeAttr(\"menu.text.bg\"),\n\t\tpadding:     Padding{4, 2},\n\t}\n\ti.BorderFg = ui.ThemeAttr(\"menu.border.fg\")\n\ti.BorderLabelFg = ui.ThemeAttr(\"menu.label.fg\")\n\ti.calcSize()\n\treturn i\n}\n\nfunc (i *Input) calcSize() {\n\ti.Height = 3 // minimum height\n\ti.Width = i.MaxLen + (i.padding[0] * 2)\n}\n\nfunc (i *Input) Buffer() ui.Buffer {\n\tvar cell ui.Cell\n\tbuf := i.Block.Buffer()\n\n\tx := i.Block.X + i.padding[0]\n\ty := i.Block.Y + 1\n\tfor _, ch := range i.Data {\n\t\tcell = ui.Cell{Ch: ch, Fg: i.TextFgColor, Bg: i.TextBgColor}\n\t\tbuf.Set(x, y, cell)\n\t\tx++\n\t}\n\n\treturn buf\n}\n\nfunc (i *Input) Stream() chan string {\n\ti.stream = make(chan string)\n\treturn i.stream\n}\n\nfunc (i *Input) KeyPress(e ui.Event) {\n\tch := strings.Replace(e.Path, \"/sys/kbd/\", \"\", -1)\n\tif ch == \"C-8\" {\n\t\tidx := len(i.Data) - 1\n\t\tif idx > -1 {\n\t\t\ti.Data = i.Data[0:idx]\n\t\t\ti.stream <- i.Data\n\t\t}\n\t\tui.Render(i)\n\t\treturn\n\t}\n\tif len(i.Data) >= i.MaxLen {\n\t\treturn\n\t}\n\tif strings.Contains(input_chars, ch) {\n\t\ti.Data += ch\n\t\ti.stream <- i.Data\n\t\tui.Render(i)\n\t}\n}\n\n// Setup some default handlers for menu navigation\nfunc (i *Input) InputHandlers() {\n\tui.Handle(\"/sys/kbd/\", i.KeyPress)\n}\n"
  },
  {
    "path": "widgets/menu/items.go",
    "content": "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 Item) Text() string {\n\tif m.Label != \"\" {\n\t\treturn m.Label\n\t}\n\treturn m.Val\n}\n\ntype Items []Item\n\nfunc NewItems(items ...Item) (mitems Items) {\n\tfor _, i := range items {\n\t\tmitems = append(mitems, i)\n\t}\n\treturn mitems\n}\n\n// Sort methods for Items\nfunc (m Items) Len() int      { return len(m) }\nfunc (m Items) Swap(a, b int) { m[a], m[b] = m[b], m[a] }\nfunc (m Items) Less(a, b int) bool {\n\treturn m[a].Text() < m[b].Text()\n}\n"
  },
  {
    "path": "widgets/menu/main.go",
    "content": "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\tui.Block\n\tSortItems   bool   // enable automatic sorting of menu items\n\tSelectable  bool   // whether menu is navigable\n\tSubText     string // optional text to display before items\n\tTextFgColor ui.Attribute\n\tTextBgColor ui.Attribute\n\tcursorPos   int\n\titems       Items\n\tpadding     Padding\n\ttoolTip     *ToolTip\n}\n\nfunc NewMenu() *Menu {\n\tm := &Menu{\n\t\tBlock:       *ui.NewBlock(),\n\t\tTextFgColor: ui.ThemeAttr(\"menu.text.fg\"),\n\t\tTextBgColor: ui.ThemeAttr(\"menu.text.bg\"),\n\t\tcursorPos:   0,\n\t\tpadding:     Padding{4, 2},\n\t}\n\tm.BorderFg = ui.ThemeAttr(\"menu.border.fg\")\n\tm.BorderLabelFg = ui.ThemeAttr(\"menu.label.fg\")\n\tm.X = 1\n\treturn m\n}\n\n// Append Item to Menu\nfunc (m *Menu) AddItems(items ...Item) {\n\tfor _, i := range items {\n\t\tm.items = append(m.items, i)\n\t}\n\tm.refresh()\n}\n\n// DelItem removes menu item by value or label\nfunc (m *Menu) DelItem(s string) (success bool) {\n\tfor n, i := range m.items {\n\t\tif i.Val == s || i.Label == s {\n\t\t\tm.items = append(m.items[:n], m.items[n+1:]...)\n\t\t\tsuccess = true\n\t\t\tm.refresh()\n\t\t\tbreak\n\t\t}\n\t}\n\treturn success\n}\n\n// ClearItems removes all current menu items\nfunc (m *Menu) ClearItems() {\n\tm.items = m.items[:0]\n}\n\n// Move cursor to an position by Item value or label\nfunc (m *Menu) SetCursor(s string) (success bool) {\n\tfor n, i := range m.items {\n\t\tif i.Val == s || i.Label == s {\n\t\t\tm.cursorPos = n\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// SetToolTip sets an optional tooltip string to show at bottom of screen\nfunc (m *Menu) SetToolTip(lines ...string) {\n\tm.toolTip = NewToolTip(lines...)\n}\n\nfunc (m *Menu) SelectedItem() Item {\n\treturn m.items[m.cursorPos]\n}\n\nfunc (m *Menu) SelectedValue() string {\n\treturn m.items[m.cursorPos].Val\n}\n\nfunc (m *Menu) Buffer() ui.Buffer {\n\tvar cell ui.Cell\n\tbuf := m.Block.Buffer()\n\n\ty := m.Y + m.padding[1]\n\n\tif m.SubText != \"\" {\n\t\tx := m.X + m.padding[0]\n\t\tfor i, ch := range m.SubText {\n\t\t\tcell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}\n\t\t\tbuf.Set(x+i, y, cell)\n\t\t}\n\t\ty += 2\n\t}\n\n\tfor n, item := range m.items {\n\t\tx := m.X + m.padding[0]\n\t\tfor _, ch := range item.Text() {\n\t\t\t// invert bg/fg colors on currently selected row\n\t\t\tif m.Selectable && n == m.cursorPos {\n\t\t\t\tcell = ui.Cell{Ch: ch, Fg: ui.ColorBlack, Bg: m.TextFgColor}\n\t\t\t} else {\n\t\t\t\tcell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}\n\t\t\t}\n\t\t\tbuf.Set(x, y+n, cell)\n\t\t\tx++\n\t\t}\n\t}\n\n\tif m.toolTip != nil {\n\t\tbuf.Merge(m.toolTip.Buffer())\n\t}\n\n\treturn buf\n}\n\nfunc (m *Menu) Up() {\n\tif m.cursorPos > 0 {\n\t\tm.cursorPos--\n\t\tui.Render(m)\n\t}\n}\n\nfunc (m *Menu) Down() {\n\tif m.cursorPos < (len(m.items) - 1) {\n\t\tm.cursorPos++\n\t\tui.Render(m)\n\t}\n}\n\n// Sort menu items(if enabled) and re-calculate window size\nfunc (m *Menu) refresh() {\n\tif m.SortItems {\n\t\tsort.Sort(m.items)\n\t}\n\tm.calcSize()\n\tui.Render(m)\n}\n\n// Set width and height based on menu items\nfunc (m *Menu) calcSize() {\n\tm.Width = 7 // minimum width\n\n\tvar height int\n\tfor _, i := range m.items {\n\t\ts := i.Text()\n\t\tif len(s) > m.Width {\n\t\t\tm.Width = len(s)\n\t\t}\n\t\theight++\n\t}\n\n\tif m.SubText != \"\" {\n\t\tif len(m.SubText) > m.Width {\n\t\t\tm.Width = len(m.SubText)\n\t\t}\n\t\theight += 2\n\t}\n\n\tm.Width += (m.padding[0] * 2)\n\tm.Height = height + (m.padding[1] * 2)\n}\n"
  },
  {
    "path": "widgets/menu/tooltip.go",
    "content": "package menu\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\ntype ToolTip struct {\n\tui.Block\n\tLines       []string\n\tTextFgColor ui.Attribute\n\tTextBgColor ui.Attribute\n\tpadding     Padding\n}\n\nfunc NewToolTip(lines ...string) *ToolTip {\n\tt := &ToolTip{\n\t\tBlock:       *ui.NewBlock(),\n\t\tLines:       lines,\n\t\tTextFgColor: ui.ThemeAttr(\"menu.text.fg\"),\n\t\tTextBgColor: ui.ThemeAttr(\"menu.text.bg\"),\n\t\tpadding:     Padding{2, 1},\n\t}\n\tt.BorderFg = ui.ThemeAttr(\"menu.border.fg\")\n\tt.BorderLabelFg = ui.ThemeAttr(\"menu.label.fg\")\n\tt.X = 1\n\tt.Align()\n\treturn t\n}\n\nfunc (t *ToolTip) Buffer() ui.Buffer {\n\tvar cell ui.Cell\n\tbuf := t.Block.Buffer()\n\n\ty := t.Y + t.padding[1]\n\n\tfor n, line := range t.Lines {\n\t\tx := t.X + t.padding[0]\n\t\tfor _, ch := range line {\n\t\t\tcell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}\n\t\t\tbuf.Set(x, y+n, cell)\n\t\t\tx++\n\t\t}\n\t}\n\n\treturn buf\n}\n\n// Set width and height based on screen size\nfunc (t *ToolTip) Align() {\n\tt.Width = ui.TermWidth() - (t.padding[0] * 2)\n\tt.Height = len(t.Lines) + (t.padding[1] * 2)\n\tt.Y = ui.TermHeight() - t.Height\n\n\tt.Block.Align()\n}\n"
  },
  {
    "path": "widgets/status.go",
    "content": "package widgets\n\nimport (\n\tui \"github.com/gizak/termui\"\n)\n\nvar (\n\tstatusHeight = 1\n\tstatusIter   = 3\n)\n\ntype StatusLine struct {\n\tMessage *ui.Par\n\tbg      *ui.Par\n}\n\nfunc NewStatusLine() *StatusLine {\n\tp := ui.NewPar(\"\")\n\tp.X = 2\n\tp.Border = false\n\tp.Height = statusHeight\n\tp.Bg = ui.ThemeAttr(\"header.bg\")\n\tp.TextFgColor = ui.ThemeAttr(\"header.fg\")\n\tp.TextBgColor = ui.ThemeAttr(\"header.bg\")\n\treturn &StatusLine{\n\t\tMessage: p,\n\t\tbg:      statusBg(),\n\t}\n}\n\nfunc (sl *StatusLine) Display() {\n\tui.DefaultEvtStream.ResetHandlers()\n\tdefer ui.DefaultEvtStream.ResetHandlers()\n\n\titer := statusIter\n\tui.Handle(\"/sys/kbd/\", func(ui.Event) {\n\t\tui.StopLoop()\n\t})\n\tui.Handle(\"/timer/1s\", func(ui.Event) {\n\t\titer--\n\t\tif iter <= 0 {\n\t\t\tui.StopLoop()\n\t\t}\n\t})\n\n\tui.Render(sl)\n\tui.Loop()\n}\n\n// change given message on the status line\nfunc (sl *StatusLine) Show(s string) {\n\tsl.Message.TextFgColor = ui.ThemeAttr(\"header.fg\")\n\tsl.Message.Text = s\n\tsl.Display()\n}\n\nfunc (sl *StatusLine) ShowErr(s string) {\n\tsl.Message.TextFgColor = ui.ThemeAttr(\"status.danger\")\n\tsl.Message.Text = s\n\tsl.Display()\n}\n\nfunc (sl *StatusLine) Buffer() ui.Buffer {\n\tbuf := ui.NewBuffer()\n\tbuf.Merge(sl.bg.Buffer())\n\tbuf.Merge(sl.Message.Buffer())\n\treturn buf\n}\n\nfunc (sl *StatusLine) Align() {\n\tsl.bg.SetWidth(ui.TermWidth() - 1)\n\tsl.Message.SetWidth(ui.TermWidth() - 2)\n\n\tsl.bg.Y = ui.TermHeight() - 1\n\tsl.Message.Y = ui.TermHeight() - 1\n}\n\nfunc (sl *StatusLine) Height() int { return statusHeight }\n\nfunc statusBg() *ui.Par {\n\tbg := ui.NewPar(\"\")\n\tbg.X = 1\n\tbg.Height = statusHeight\n\tbg.Border = false\n\tbg.Bg = ui.ThemeAttr(\"header.bg\")\n\treturn bg\n}\n"
  },
  {
    "path": "widgets/view.go",
    "content": "package widgets\n\nimport (\n\tui \"github.com/gizak/termui\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\ntype ToggleText interface {\n\t// returns text for toggle on/off\n\tToggle(on bool) string\n}\n\ntype TextView struct {\n\tui.Block\n\tinputStream <-chan ToggleText\n\trender      chan bool\n\ttoggleState bool\n\tText        []ToggleText // all the text\n\tTextOut     []string     // text to be displayed\n\tTextFgColor ui.Attribute\n\tTextBgColor ui.Attribute\n\tpadding     Padding\n}\n\nfunc NewTextView(lines <-chan ToggleText) *TextView {\n\tt := &TextView{\n\t\tBlock:       *ui.NewBlock(),\n\t\tinputStream: lines,\n\t\trender:      make(chan bool),\n\t\tText:        []ToggleText{},\n\t\tTextOut:     []string{},\n\t\tTextFgColor: ui.ThemeAttr(\"menu.text.fg\"),\n\t\tTextBgColor: ui.ThemeAttr(\"menu.text.bg\"),\n\t\tpadding:     Padding{4, 2},\n\t}\n\n\tt.BorderFg = ui.ThemeAttr(\"menu.border.fg\")\n\tt.BorderLabelFg = ui.ThemeAttr(\"menu.label.fg\")\n\tt.Height = ui.TermHeight()\n\tt.Width = ui.TermWidth()\n\n\tt.readInputLoop()\n\tt.renderLoop()\n\treturn t\n}\n\n// Adjusts text inside this view according to the window size. No need to call ui.Render(...)\n// after calling this method, it is called automatically\nfunc (t *TextView) Resize() {\n\tui.Clear()\n\tt.Height = ui.TermHeight()\n\tt.Width = ui.TermWidth()\n\tt.render <- true\n}\n\n// Toggles text inside this view. No need to call ui.Render(...) after calling this method,\n// it is called automatically\nfunc (t *TextView) Toggle() {\n\tt.toggleState = !t.toggleState\n\tt.render <- true\n}\n\nfunc (t *TextView) Buffer() ui.Buffer {\n\tvar cell ui.Cell\n\tbuf := t.Block.Buffer()\n\n\tx := t.Block.X + t.padding[0]\n\ty := t.Block.Y + t.padding[1]\n\n\tfor _, line := range t.TextOut {\n\t\tfor _, ch := range line {\n\t\t\tcell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}\n\t\t\tbuf.Set(x, y, cell)\n\t\t\tx = x + runewidth.RuneWidth(ch)\n\t\t}\n\t\tx = t.Block.X + t.padding[0]\n\t\ty++\n\t}\n\treturn buf\n}\n\nfunc (t *TextView) renderLoop() {\n\tgo func() {\n\t\tfor range t.render {\n\t\t\tmaxWidth := t.Width - (t.padding[0] * 2)\n\t\t\theight := t.Height - (t.padding[1] * 2)\n\t\t\tt.TextOut = []string{}\n\t\t\tfor i := len(t.Text) - 1; i >= 0; i-- {\n\t\t\t\tlines := splitLine(t.Text[i].Toggle(t.toggleState), maxWidth)\n\t\t\t\tt.TextOut = append(lines, t.TextOut...)\n\t\t\t\tif len(t.TextOut) > height {\n\t\t\t\t\tt.TextOut = t.TextOut[:height]\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tui.Render(t)\n\t\t}\n\t}()\n}\n\nfunc (t *TextView) readInputLoop() {\n\tgo func() {\n\t\tfor line := range t.inputStream {\n\t\t\tt.Text = append(t.Text, line)\n\t\t\tt.render <- true\n\t\t}\n\t\tclose(t.render)\n\t}()\n}\n\nfunc splitLine(line string, lineSize int) []string {\n\tif line == \"\" {\n\t\treturn []string{}\n\t}\n\n\tvar lines []string\n\tfor {\n\t\tif len(line) <= lineSize {\n\t\t\tlines = append(lines, line)\n\t\t\treturn lines\n\t\t}\n\t\tlines = append(lines, line[:lineSize])\n\t\tline = line[lineSize:]\n\t}\n}\n"
  },
  {
    "path": "widgets/view_test.go",
    "content": "package widgets\n\nimport \"testing\"\n\nfunc TestSplitEmptyLine(t *testing.T) {\n\n\tresult := splitLine(\"\", 5)\n\tif len(result) != 0 {\n\t\tt.Errorf(\"expected: 0 lines, got: %d\", len(result))\n\t}\n}\n\nfunc TestSplitLineShorterThanLimit(t *testing.T) {\n\n\tresult := splitLine(\"hello\", 7)\n\tif len(result) != 1 {\n\t\tt.Errorf(\"expected: 0 lines, got: %d\", len(result))\n\t}\n}\n\nfunc TestSplitLineLongerThanLimit(t *testing.T) {\n\n\tresult := splitLine(\"hello\", 3)\n\tif len(result) != 2 {\n\t\tt.Errorf(\"expected: 0 lines, got: %d\", len(result))\n\t}\n}\n\nfunc TestSplitLineSameAsLimit(t *testing.T) {\n\n\tresult := splitLine(\"hello\", 5)\n\tif len(result) != 1 {\n\t\tt.Errorf(\"expected: 0 lines, got: %d\", len(result))\n\t}\n}\n"
  }
]