Compare commits

..

66 Commits
v0.3 ... v0.5

Author SHA1 Message Date
Bradley Cicenas
a87bdce0fe update keybindings in readme 2017-03-15 12:00:40 +10:00
Bradley Cicenas
2228188ebf v0.5 2017-03-15 10:08:26 +10:00
Bradley Cicenas
e94a9c0cc2 remove redundant bool comparisons 2017-03-15 10:06:52 +10:00
Bradley Cicenas
e82d77ecb0 add option for color inversion 2017-03-15 10:02:46 +10:00
Bradley Cicenas
50b4181866 update circle config 2017-03-15 09:16:04 +10:00
Bradley Cicenas
1285288b9e add validation to sort field option 2017-03-15 08:49:11 +10:00
Bradley Cicenas
2a709577bd add options to readme 2017-03-15 08:41:45 +10:00
Bradley Cicenas
38599bbd19 add keymap, handle wrapper for common keybindings 2017-03-15 08:34:58 +10:00
Bradley Cicenas
b3cdb33efc add explicit version to Dockerfile, circleci 2017-03-15 08:34:58 +10:00
bradley
0ac70c96eb Merge pull request #37 from drAlberT/patch-1
Improve suggested "docker run" cmd
2017-03-13 20:36:42 +11:00
Emiliano 'AlberT' Gabrielli
36a5bbdfe1 Improve suggested "docker run" cmd
- make it use a given name "ctop"
- make it feasible for an `alias`
2017-03-13 09:31:15 +01:00
Bradley Cicenas
3553b0af9d cap cpu gauge % to 100 in compact view 2017-03-13 09:00:17 +11:00
Bradley Cicenas
ca61ec712e prepopulate filter input with current filter, add esc handler 2017-03-13 08:32:33 +11:00
Bradley Cicenas
06c4b24212 add 0..9 to valid input chars 2017-03-13 08:31:51 +11:00
Bradley Cicenas
12fa716825 add y pos scrolling to expanded view 2017-03-13 07:53:17 +11:00
Bradley Cicenas
8327406069 add sort by pid count 2017-03-12 21:11:19 +11:00
Bradley Cicenas
2134110224 add static width for specific columns 2017-03-12 20:58:56 +11:00
Bradley Cicenas
77c3d00e67 update io labels 2017-03-12 16:31:12 +11:00
Bradley Cicenas
85eb5228ae append build steps 2017-03-12 15:00:52 +11:00
bradley
3a3950e395 Merge pull request #36 from kevinschoon/vendoring
vendor dependencies with glide
2017-03-12 14:49:13 +11:00
Kevin Schoon
eaac079b15 vendor dependencies with glide 2017-03-12 10:15:21 +07:00
bradley
ab1ccb3cd8 Merge pull request #35 from f1yegor/feature/more-stats
add pids, IO stat
2017-03-12 14:07:34 +11:00
f1yegor
dbaebe0192 add pids, IO stat 2017-03-12 02:35:40 +01:00
bradley
d5ef818c8d Merge pull request #31 from scriptnull/master
adds more commandline arguments
2017-03-11 15:01:33 +11:00
Vishnu Bharathi
8203d0b883 adds more commandline arguments 2017-03-11 00:09:06 +05:30
Bradley Cicenas
b28beed3ee v0.4.1 2017-03-10 20:20:00 +11:00
Bradley Cicenas
2e51406d00 add lock to container map in dockersource 2017-03-10 20:10:39 +11:00
Bradley Cicenas
c84b52ce40 add docker image usage to readme 2017-03-10 20:00:00 +11:00
Bradley Cicenas
4ee8cf621a use latest parsed version in dockerfile 2017-03-10 19:51:31 +11:00
Bradley Cicenas
192298c045 add circleci config for docker build 2017-03-10 19:45:04 +11:00
Bradley Cicenas
258536740d build docker image from alpine 2017-03-10 19:37:46 +11:00
bradley
ef69744249 Merge pull request #23 from firecat53/patch-1
Update Dockerfile
2017-03-10 19:19:20 +11:00
Scott Hansen
07f95a04b0 Update Dockerfile
It may not be as easy to read, but combining the three RUN statements together saves you some substantial megabytes:

    ctop_deb_orig        latest              149596353f88        4 seconds ago       187 MB
    ctop_deb_new        latest              d01f954b3adc        2 minutes ago       139 MB

This is because each RUN statement is creating a whole new layer in the image, so you're actually hanging on to all the stuff you were trying to get rid of!

Scott
2017-03-09 20:38:01 -08:00
Bradley Cicenas
b2184bbc6d fixes out of bounds error on filtered selection #7 2017-03-10 12:01:13 +11:00
Bradley Cicenas
96b01eb3b9 add support for TLS via NewClientFromEnv(), remove dockerHost config param 2017-03-10 12:01:13 +11:00
bradley
03d4869361 Merge pull request #14 from francislavoie/master
Add Dockerfile
2017-03-10 11:57:15 +11:00
bradley
4b7257908f Merge pull request #15 from huguesalary/master
Fixing a typo
2017-03-10 11:55:49 +11:00
Hugues Alary
1875013a76 Fixing a typo 2017-03-09 16:16:40 -08:00
Francis Lavoie
dab2f926b9 Add Dockerfile 2017-03-09 19:08:35 -05:00
Bradley Cicenas
ddce54f991 add AUR to README
fix typo
2017-03-10 09:24:44 +11:00
Bradley Cicenas
168e8f3aae implement Buffer() method for ctop header 2017-03-09 19:33:25 +11:00
Bradley Cicenas
ecc37a2f99 ensure Loop() is started before feeding refresh chan 2017-03-09 18:39:11 +11:00
Bradley Cicenas
2f17a9d689 add ctrl+c exit handler 2017-03-09 16:29:34 +11:00
Bradley Cicenas
8a6808c804 trim grid screencap 2017-03-09 12:22:37 +11:00
Bradley Cicenas
3ca94b50cd add LICENSE 2017-03-09 10:41:32 +11:00
Bradley Cicenas
0e3fe88bb4 use consistent case for ctop name 2017-03-09 10:40:35 +11:00
Bradley Cicenas
b9b904626c update readme 2017-03-09 10:37:02 +11:00
bradley
e195828f92 resize grid screencap 2017-03-09 10:28:02 +11:00
Bradley Cicenas
1d176d46c4 remove sleep from mocksource container creation 2017-03-09 10:26:29 +11:00
Bradley Cicenas
7026193f8e add expanded view screen cap, docpage 2017-03-09 10:25:29 +11:00
Bradley Cicenas
d9b4295176 update grid screencap 2017-03-09 10:13:57 +11:00
Bradley Cicenas
92cc7bc849 add aggression multiplier to mock collector 2017-03-09 09:33:44 +11:00
Bradley Cicenas
70790e88ae v0.4 2017-03-08 22:20:30 +11:00
Bradley Cicenas
bcf05b7f42 fix panic on row removal 2017-03-08 22:19:43 +11:00
bradley
9df3ff2aa0 set static logo width 2017-03-08 19:19:38 +11:00
Bradley Cicenas
2b80832a36 add pagination support for compact view 2017-03-08 18:45:31 +11:00
Bradley Cicenas
a6ee6edb1d move MaxRows() method into cgrid 2017-03-08 11:52:31 +11:00
Bradley Cicenas
d7f9f715bb fix cursor highlighting for newly filtered containers 2017-03-08 11:46:39 +11:00
Bradley Cicenas
2d2d58d47f filter Containers in place 2017-03-08 11:26:22 +11:00
Bradley Cicenas
bf4d59c251 clear screen conditionally 2017-03-08 11:10:38 +11:00
Bradley Cicenas
b8eb386360 add global default ColorMap 2017-03-08 10:40:03 +11:00
Bradley Cicenas
02610c59da move logo to docs folder
fix logo path
2017-03-08 08:41:20 +11:00
Bradley Cicenas
71768b498c update release url 2017-03-07 20:22:44 +11:00
Bradley Cicenas
57e49ea2c6 add logo to readme 2017-03-07 20:10:19 +11:00
bradley
5b25f931df center screencap in readme 2017-03-07 14:52:28 +11:00
Bradley Cicenas
4af33fdf12 add screencaps to readme 2017-03-07 14:48:44 +11:00
43 changed files with 771 additions and 232 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
ctop
.idea

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM quay.io/vektorcloud/glibc:latest
ARG CTOP_VERSION=0.5
ENV CTOP_URL https://github.com/bcicen/ctop/releases/download/v${CTOP_VERSION}/ctop-${CTOP_VERSION}-linux-amd64
RUN echo $CTOP_URL && \
wget -q $CTOP_URL -O /ctop && \
chmod +x /ctop
ENTRYPOINT ["/ctop"]

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2017 VektorLab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +1,15 @@
# ctop
<p align="center"><img width="200px" src="/_docs/img/logo.png" alt="ctop"/></p>
#
Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
as well as an [expanded view][expanded_view] for inspecting a specific container.
`ctop` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases.
## Install
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
@@ -9,7 +17,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux
```bash
wget https://github.com/bcicen/ctop/releases/download/v0.1/ctop-0.1-linux-amd64 -O ctop
wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O ctop
sudo mv ctop /usr/local/bin/
sudo chmod +x /usr/local/bin/ctop
```
@@ -17,27 +25,59 @@ sudo chmod +x /usr/local/bin/ctop
#### OS X
```bash
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.1/ctop-0.1-darwin-amd64
curl -Lo ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64
sudo mv ctop /usr/local/bin/
sudo chmod +x /usr/local/bin/ctop
```
or run via Docker:
```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
```
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop/)
## Building
To build `ctop` from source, ensure you have a recent version of [glide](http://glide.sh/) installed and run:
```bash
git clone https://github.com/bcicen/ctop.git $GOPATH/src/github.com/bcicen/ctop && \
cd $GOPATH/src/github.com/bcicen/ctop && \
glide install && \
go build
```
## Usage
cTop requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
`ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
```bash
export DOCKER_HOST=tcp://127.0.0.1:4243
ctop
```
### Options
Option | Description
--- | ---
-a | show active containers only
-f <string> | set an initial filter string
-h | display help dialog
-i | invert default colors
-r | reverse container sort order
-s | select initial container sort field
-v | output version information and exit
### Keybindings
Key | Action
--- | ---
a | Toggle display of all (running and non-running) containers
f | Filter displayed containers
H | Toggle cTop header
f | Filter displayed containers (`esc` to clear when open)
H | Toggle ctop header
h | Open help dialog
s | Select container sort field
r | Reverse container sort order
q | Quit cTop
q | Quit ctop
[expanded_view]: _docs/expanded.md

View File

@@ -1 +1 @@
0.3
0.5

4
_docs/expanded.md Normal file
View File

@@ -0,0 +1,4 @@
# Expanded View
ctop provides an expanded, rolling view for following container metrics
<p align="center"><img width="80%" src="img/expanded.gif" alt="ctop"/></p>

BIN
_docs/img/expanded.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

BIN
_docs/img/grid.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

BIN
_docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

23
circle.yml Normal file
View File

@@ -0,0 +1,23 @@
machine:
services:
- docker
environment:
IMAGE_NAME: quay.io/vektorlab/ctop
dependencies:
override:
- docker info
- docker build --build-arg CTOP_VERSION=$(cat VERSION) -t ctop .
test:
override:
- docker run -ti ctop -v
deployment:
hub:
branch: master
commands:
- docker tag ctop ${IMAGE_NAME}:latest
- docker tag ctop ${IMAGE_NAME}:$(cat VERSION)
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
- docker push ${IMAGE_NAME}

58
colors.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"regexp"
ui "github.com/gizak/termui"
)
/*
Valid colors:
ui.ColorDefault
ui.ColorBlack
ui.ColorRed
ui.ColorGreen
ui.ColorYellow
ui.ColorBlue
ui.ColorMagenta
ui.ColorCyan
ui.ColorWhite
*/
var ColorMap = map[string]ui.Attribute{
"fg": ui.ColorWhite,
"bg": ui.ColorDefault,
"block.bg": ui.ColorDefault,
"border.bg": ui.ColorDefault,
"border.fg": ui.ColorWhite,
"label.bg": ui.ColorDefault,
"label.fg": ui.ColorGreen,
"menu.text.fg": ui.ColorWhite,
"menu.text.bg": ui.ColorDefault,
"menu.border.fg": ui.ColorCyan,
"menu.label.fg": ui.ColorGreen,
"header.fg": ui.ColorBlack,
"header.bg": ui.ColorWhite,
"gauge.bar.bg": ui.ColorGreen,
"gauge.percent.fg": ui.ColorWhite,
"linechart.axes.fg": ui.ColorDefault,
"linechart.line.fg": ui.ColorGreen,
"mbarchart.bar.bg": ui.ColorGreen,
"mbarchart.num.fg": ui.ColorWhite,
"mbarchart.text.fg": ui.ColorWhite,
"par.text.fg": ui.ColorWhite,
"par.text.bg": ui.ColorDefault,
"par.text.hi": ui.ColorBlack,
"sparkline.line.fg": ui.ColorGreen,
"sparkline.title.fg": ui.ColorWhite,
}
func InvertColorMap() {
re := regexp.MustCompile(".*.fg")
for k, _ := range ColorMap {
if re.FindAllString(k, 1) != nil {
ColorMap[k] = ui.ColorBlack
}
}
ColorMap["par.text.hi"] = ui.ColorWhite
}

View File

@@ -2,11 +2,6 @@ package config
// defaults
var params = []*Param{
&Param{
Key: "dockerHost",
Val: getEnv("DOCKER_HOST", "unix:///var/run/docker.sock"),
Label: "Docker API URL",
},
&Param{
Key: "filterStr",
Val: "",

View File

@@ -15,7 +15,7 @@ var switches = []*Switch{
&Switch{
Key: "enableHeader",
Val: true,
Label: "Enable cTop Status Line",
Label: "Enable Status Header",
},
&Switch{
Key: "loggingEnabled",

View File

@@ -14,6 +14,7 @@ type Container struct {
Widgets *compact.Compact
updater cwidgets.WidgetUpdater
collector metrics.Collector
display bool // display this container in compact view
}
func NewContainer(id string, collector metrics.Collector) *Container {

View File

@@ -6,7 +6,7 @@ import (
type GridCursor struct {
selectedID string // id of currently selected container
containers Containers
filtered Containers
cSource ContainerSource
}
@@ -16,64 +16,117 @@ func NewGridCursor() *GridCursor {
}
}
func (gc *GridCursor) Len() int { return len(gc.containers) }
func (gc *GridCursor) Selected() *Container { return gc.containers[gc.Idx()] }
func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) RefreshContainers() {
gc.containers = gc.cSource.All().Filter()
func (gc *GridCursor) Selected() *Container {
idx := gc.Idx()
if idx < gc.Len() {
return gc.filtered[idx]
}
return nil
}
// Refresh containers from source
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
oldLen := gc.Len()
// Containers filtered by display bool
gc.filtered = Containers{}
var cursorVisible bool
for _, c := range gc.cSource.All() {
if c.display {
if c.Id == gc.selectedID {
cursorVisible = true
}
gc.filtered = append(gc.filtered, c)
}
}
if oldLen != gc.Len() {
lenChanged = true
}
if !cursorVisible {
gc.Reset()
}
if gc.selectedID == "" {
gc.Reset()
}
return lenChanged
}
// Set an initial cursor position, if possible
func (gc *GridCursor) Reset() {
for _, c := range gc.cSource.All() {
c.Widgets.Name.UnHighlight()
}
if gc.Len() > 0 {
gc.selectedID = gc.containers[0].Id
gc.containers[0].Widgets.Name.Highlight()
gc.selectedID = gc.filtered[0].Id
gc.filtered[0].Widgets.Name.Highlight()
}
}
// Return current cursor index
func (gc *GridCursor) Idx() int {
for n, c := range gc.containers {
for n, c := range gc.filtered {
if c.Id == gc.selectedID {
return n
}
}
gc.Reset()
return 0
}
func (gc *GridCursor) ScrollPage() {
// skip scroll if no need to page
if gc.Len() < cGrid.MaxRows() {
cGrid.Offset = 0
return
}
idx := gc.Idx()
// page down
if idx >= cGrid.Offset+cGrid.MaxRows() {
cGrid.Offset++
cGrid.Align()
}
// page up
if idx < cGrid.Offset {
cGrid.Offset--
cGrid.Align()
}
}
func (gc *GridCursor) Up() {
idx := gc.Idx()
// decrement if possible
if idx <= 0 {
if idx <= 0 { // already at top
return
}
active := gc.containers[idx]
next := gc.containers[idx-1]
active := gc.filtered[idx]
next := gc.filtered[idx-1]
active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
}
func (gc *GridCursor) Down() {
idx := gc.Idx()
// increment if possible
if idx >= (gc.Len() - 1) {
if idx >= gc.Len()-1 { // already at bottom
return
}
if idx >= maxRows()-1 {
return
}
active := gc.containers[idx]
next := gc.containers[idx+1]
active := gc.filtered[idx]
next := gc.filtered[idx+1]
active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
}

View File

@@ -14,7 +14,6 @@ func NewGaugeCol() *GaugeCol {
g.Border = false
g.Percent = 0
g.PaddingBottom = 0
g.BarColor = ui.ColorGreen
g.Label = "-"
return &GaugeCol{g}
}

View File

@@ -4,7 +4,7 @@ import (
ui "github.com/gizak/termui"
)
var header = NewCompactHeader()
var header *CompactHeader
type CompactGrid struct {
ui.GridBufferer
@@ -12,36 +12,43 @@ type CompactGrid struct {
X, Y int
Width int
Height int
cursorID string
Offset int // starting row offset
}
func NewCompactGrid() *CompactGrid {
header = NewCompactHeader() // init column header
return &CompactGrid{}
}
func (cg *CompactGrid) Align() {
// update row y pos recursively
y := cg.Y
for _, r := range cg.Rows {
if cg.Offset >= len(cg.Rows) {
cg.Offset = 0
}
// update row ypos, width recursively
for _, r := range cg.pageRows() {
r.SetY(y)
y += r.GetHeight()
}
// update row width recursively
for _, r := range cg.Rows {
r.SetWidth(cg.Width)
}
}
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{header} }
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) }
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
func (cg *CompactGrid) SetX(x int) { cg.X = x }
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y }
func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) {
rows = append(rows, header)
rows = append(rows, cg.Rows[cg.Offset:]...)
return rows
}
func (cg *CompactGrid) Buffer() ui.Buffer {
buf := ui.NewBuffer()
for _, r := range cg.Rows {
for _, r := range cg.pageRows() {
buf.Merge(r.Buffer())
}
return buf

View File

@@ -12,7 +12,7 @@ type CompactHeader struct {
}
func NewCompactHeader() *CompactHeader {
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX"}
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
ch := &CompactHeader{}
ch.Height = 2
for _, f := range fields {
@@ -27,13 +27,13 @@ func (ch *CompactHeader) GetHeight() int {
func (ch *CompactHeader) SetWidth(w int) {
x := ch.X
autoWidth := calcWidth(w, 5)
autoWidth := calcWidth(w)
for n, col := range ch.pars {
// set status column to static width
if n == 0 {
// set column to static width
if colWidths[n] != 0 {
col.SetX(x)
col.SetWidth(statusWidth)
x += statusWidth
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)

View File

@@ -15,6 +15,8 @@ type Compact struct {
Cpu *GaugeCol
Memory *GaugeCol
Net *TextCol
IO *TextCol
Pids *TextCol
X, Y int
Width int
Height int
@@ -32,6 +34,8 @@ func NewCompact(id string) *Compact {
Cpu: NewGaugeCol(),
Memory: NewGaugeCol(),
Net: NewTextCol("-"),
IO: NewTextCol("-"),
Pids: NewTextCol("-"),
X: 1,
Height: 1,
}
@@ -59,6 +63,8 @@ func (row *Compact) SetMetrics(m metrics.Metrics) {
row.SetCPU(m.CPUUtil)
row.SetNet(m.NetRx, m.NetTx)
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
row.SetPids(m.Pids)
}
// Set gauges, counters to default unread values
@@ -66,6 +72,8 @@ func (row *Compact) Reset() {
row.Cpu.Reset()
row.Memory.Reset()
row.Net.Reset()
row.IO.Reset()
row.Pids.Reset()
}
func (row *Compact) GetHeight() int {
@@ -91,13 +99,12 @@ func (row *Compact) SetWidth(width int) {
return
}
x := row.X
autoWidth := calcWidth(width, 5)
autoWidth := calcWidth(width)
for n, col := range row.all() {
// set status column to static width
if n == 0 {
if colWidths[n] != 0 {
col.SetX(x)
col.SetWidth(statusWidth)
x += statusWidth
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)
@@ -116,7 +123,8 @@ func (row *Compact) Buffer() ui.Buffer {
buf.Merge(row.Cpu.Buffer())
buf.Merge(row.Memory.Buffer())
buf.Merge(row.Net.Buffer())
buf.Merge(row.IO.Buffer())
buf.Merge(row.Pids.Buffer())
return buf
}
@@ -128,5 +136,7 @@ func (row *Compact) all() []ui.GridBufferer {
row.Cpu,
row.Memory,
row.Net,
row.IO,
row.Pids,
}
}

View File

@@ -13,12 +13,25 @@ func (row *Compact) SetNet(rx int64, tx int64) {
row.Net.Set(label)
}
func (row *Compact) SetIO(read int64, write int64) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(read), cwidgets.ByteFormat(write))
row.IO.Set(label)
}
func (row *Compact) SetPids(val int) {
label := fmt.Sprintf("%s", strconv.Itoa(val))
row.Pids.Set(label)
}
func (row *Compact) SetCPU(val int) {
row.Cpu.BarColor = colorScale(val)
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
if val < 5 {
val = 5
row.Cpu.BarColor = ui.ColorBlack
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
}
if val > 100 {
val = 100
}
row.Cpu.Percent = val
}
@@ -29,7 +42,7 @@ func (row *Compact) SetMem(val int64, limit int64, percent int) {
percent = 5
row.Memory.BarColor = ui.ColorBlack
} else {
row.Memory.BarColor = ui.ColorGreen
row.Memory.BarColor = ui.ThemeAttr("gauge.bar.bg")
}
row.Memory.Percent = percent
}

View File

@@ -17,7 +17,7 @@ func NewTextCol(s string) *TextCol {
}
func (w *TextCol) Highlight() {
w.TextFgColor = ui.ThemeAttr("par.text.bg")
w.TextFgColor = ui.ThemeAttr("par.text.hi")
w.TextBgColor = ui.ThemeAttr("par.text.fg")
}

View File

@@ -9,10 +9,29 @@ import (
const colSpacing = 1
// Calculate per-column width, given total width and number of items
func calcWidth(width, items int) int {
spacing := colSpacing * items
return (width - statusWidth - spacing) / items
// per-column width. 0 == auto width
var colWidths = []int{
3, // status
0, // name
0, // cid
0, // cpu
0, // memory
0, // net
0, // io
4, // pids
}
// Calculate per-column width, given total width
func calcWidth(width int) int {
spacing := colSpacing * len(colWidths)
var staticCols int
for _, w := range colWidths {
width -= w
if w == 0 {
staticCols += 1
}
}
return (width - spacing) / staticCols
}
func centerParText(p *ui.Par) {

View File

@@ -17,8 +17,6 @@ func NewCpu() *Cpu {
cpu.Width = colWidth[0]
cpu.X = 0
cpu.DataLabels = cpu.hist.Labels
cpu.AxesColor = ui.ColorDefault
cpu.LineColor = ui.ColorGreen
// hack to force the default minY scale to 0
tmpData := []float64{20}

View File

@@ -15,8 +15,8 @@ func NewInfo(id string) *Info {
p := ui.NewTable()
p.Height = 4
p.Width = colWidth[0]
p.FgColor = ui.ColorWhite
p.Seperator = false
p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false
i := &Info{p, make(map[string]string)}
i.Set("id", id)
return i

51
cwidgets/expanded/io.go Normal file
View File

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

View File

@@ -17,6 +17,8 @@ type Expanded struct {
Net *Net
Cpu *Cpu
Mem *Mem
IO *IO
X, Y int
Width int
}
@@ -29,30 +31,59 @@ func NewExpanded(id string) *Expanded {
Net: NewNet(),
Cpu: NewCpu(),
Mem: NewMem(),
IO: NewIO(),
Width: ui.TermWidth(),
}
}
func (e *Expanded) SetWidth(w int) {
e.Width = w
func (e *Expanded) Up() {
if e.Y < 0 {
e.Y++
e.Align()
ui.Render(e)
}
}
func (e *Expanded) SetMeta(k, v string) {
e.Info.Set(k, v)
func (e *Expanded) Down() {
if e.Y > (ui.TermHeight() - e.GetHeight()) {
e.Y--
e.Align()
ui.Render(e)
}
}
func (e *Expanded) SetWidth(w int) { e.Width = w }
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
func (e *Expanded) SetMetrics(m metrics.Metrics) {
e.Cpu.Update(m.CPUUtil)
e.Net.Update(m.NetRx, m.NetTx)
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
}
// Return total column height
func (e *Expanded) GetHeight() (h int) {
h += e.Info.Height
h += e.Net.Height
h += e.Cpu.Height
h += e.Mem.Height
h += e.IO.Height
return h
}
func (e *Expanded) Align() {
y := 0
// reset offset if needed
if e.GetHeight() <= ui.TermHeight() {
e.Y = 0
}
y := e.Y
for _, i := range e.all() {
i.SetY(y)
y += i.GetHeight()
}
if e.Width > colWidth[0] {
colWidth[1] = e.Width - (colWidth[0] + 1)
}
@@ -74,6 +105,7 @@ func (e *Expanded) Buffer() ui.Buffer {
buf.Merge(e.Cpu.Buffer())
buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
return buf
}
@@ -83,6 +115,7 @@ func (e *Expanded) all() []ui.GridBufferer {
e.Cpu,
e.Mem,
e.Net,
e.IO,
}
}

View File

@@ -57,7 +57,6 @@ func newMemLabel() *ui.Par {
p.Border = false
p.Height = 1
p.Width = 20
p.TextFgColor = ui.ColorDefault
return p
}
@@ -67,9 +66,6 @@ func newMemChart() *ui.MBarChart {
mbar.Border = false
mbar.BarGap = 1
mbar.BarWidth = 6
mbar.TextColor = ui.ColorDefault
mbar.BarColor[0] = ui.ColorGreen
mbar.BarColor[1] = ui.ColorBlack
mbar.NumColor[1] = ui.ColorBlack

View File

@@ -26,14 +26,12 @@ func NewNet() *Net {
rx.Title = "RX"
rx.Height = 1
rx.Data = net.rxHist.Data
rx.TitleColor = ui.ColorDefault
rx.LineColor = ui.ColorGreen
tx := ui.NewSparkline()
tx.Title = "TX"
tx.Height = 1
tx.Data = net.txHist.Data
tx.TitleColor = ui.ColorDefault
tx.LineColor = ui.ColorYellow
net.Lines = []ui.Sparkline{rx, tx}

View File

@@ -3,8 +3,8 @@ package main
import (
"sort"
"strings"
"sync"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/metrics"
"github.com/fsouza/go-dockerclient"
)
@@ -18,11 +18,12 @@ type DockerContainerSource struct {
client *docker.Client
containers map[string]*Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewDockerContainerSource() *DockerContainerSource {
// init docker client
client, err := docker.NewClient(config.GetVal("dockerHost"))
client, err := docker.NewClientFromEnv()
if err != nil {
panic(err)
}
@@ -30,9 +31,10 @@ func NewDockerContainerSource() *DockerContainerSource {
client: client,
containers: make(map[string]*Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
}
cm.refreshAll()
go cm.Loop()
cm.refreshAll()
go cm.watchEvents()
return cm
}
@@ -113,29 +115,38 @@ func (cm *DockerContainerSource) MustGet(id string) *Container {
collector := metrics.NewDocker(cm.client, id)
// create container
c = NewContainer(id, collector)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
}
return c
}
// Get a single container, by ID
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
cm.lock.Unlock()
return c, ok
}
// Remove containers by ID
func (cm *DockerContainerSource) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
func (cm *DockerContainerSource) All() (containers Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
cm.lock.Unlock()
sort.Sort(containers)
containers.Filter()
return containers
}

85
glide.lock generated Normal file
View File

@@ -0,0 +1,85 @@
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
updated: 2017-03-12T09:53:35.212073637+07:00
imports:
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/docker/docker
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
subpackages:
- api/types
- api/types/blkiodev
- api/types/container
- api/types/filters
- api/types/mount
- api/types/network
- api/types/registry
- api/types/strslice
- api/types/swarm
- api/types/versions
- opts
- pkg/archive
- pkg/fileutils
- pkg/homedir
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/system
- pkg/term
- pkg/term/windows
- name: github.com/docker/go-connections
version: a2afab9802043837035592f1c24827fb70766de9
subpackages:
- nat
- name: github.com/docker/go-units
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
- name: github.com/fsouza/go-dockerclient
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
- name: github.com/gizak/termui
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
repo: https://github.com/bcicen/termui
vcs: git
- name: github.com/hashicorp/go-cleanhttp
version: 3573b8b52aa7b37b9358d966a898feb387f62437
- name: github.com/jgautheron/codename-generator
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
- name: github.com/maruel/panicparse
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
subpackages:
- stack
- name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/Microsoft/go-winio
version: fff283ad5116362ca252298cfc9b95828956d85d
- name: github.com/mitchellh/go-wordwrap
version: ad45545899c7b13c020ea92b2072220eefad42b8
- name: github.com/nsf/termbox-go
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
- name: github.com/nu7hatch/gouuid
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
- name: github.com/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/opencontainers/runc
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/Sirupsen/logrus
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
- name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
subpackages:
- unix
- windows
testImports: []

11
glide.yaml Normal file
View File

@@ -0,0 +1,11 @@
package: github.com/bcicen/ctop
import:
- package: github.com/fsouza/go-dockerclient
- package: github.com/gizak/termui
version: barchart-numfmt
repo: https://github.com/bcicen/termui
vcs: git
- package: github.com/jgautheron/codename-generator
- package: github.com/nu7hatch/gouuid
- package: github.com/op/go-logging
version: ^1.0.0

86
grid.go
View File

@@ -6,11 +6,7 @@ import (
ui "github.com/gizak/termui"
)
func maxRows() int {
return ui.TermHeight() - 2 - cGrid.Y
}
func RedrawRows() {
func RedrawRows(clr bool) {
// reinit body rows
cGrid.Clear()
@@ -23,25 +19,16 @@ func RedrawRows() {
}
cGrid.SetY(y)
var cursorVisible bool
max := maxRows()
for n, c := range cursor.containers {
if n >= max {
break
}
for _, c := range cursor.filtered {
cGrid.AddRows(c.Widgets)
if c.Id == cursor.selectedID {
cursorVisible = true
}
}
if !cursorVisible {
cursor.Reset()
}
if clr {
ui.Clear()
log.Debugf("screen cleared")
}
if config.GetSwitchVal("enableHeader") {
header.Render()
ui.Render(header)
}
cGrid.Align()
ui.Render(cGrid)
@@ -57,22 +44,27 @@ func ExpandView(c *Container) {
ex.Align()
ui.Render(ex)
ui.Handle("/timer/1s", func(ui.Event) {
ui.Render(ex)
})
HandleKeys("up", ex.Up)
HandleKeys("down", ex.Down)
ui.Handle("/sys/kbd/", func(ui.Event) { ui.StopLoop() })
ui.Handle("/timer/1s", func(ui.Event) { ui.Render(ex) })
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ex.SetWidth(ui.TermWidth())
ex.Align()
log.Infof("resize: width=%v max-rows=%v", ex.Width, maxRows())
log.Infof("resize: width=%v max-rows=%v", ex.Width, cGrid.MaxRows())
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
ui.Loop()
c.SetUpdater(c.Widgets)
}
func RefreshDisplay() {
needsClear := cursor.RefreshContainers()
RedrawRows(needsClear)
}
func Display() bool {
var menu func()
var expand bool
@@ -83,23 +75,23 @@ func Display() bool {
// initial draw
header.Align()
cursor.RefreshContainers()
RedrawRows()
RedrawRows(true)
ui.Handle("/sys/kbd/<up>", func(ui.Event) {
cursor.Up()
})
ui.Handle("/sys/kbd/<down>", func(ui.Event) {
cursor.Down()
HandleKeys("up", cursor.Up)
HandleKeys("down", cursor.Down)
HandleKeys("exit", ui.StopLoop)
HandleKeys("help", func() {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
expand = true
ui.StopLoop()
})
ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers")
cursor.RefreshContainers()
RedrawRows()
RefreshDisplay()
})
ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected())
@@ -108,16 +100,9 @@ func Display() bool {
menu = FilterMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/h", func(ui.Event) {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/H", func(ui.Event) {
config.Toggle("enableHeader")
RedrawRows()
})
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.StopLoop()
RedrawRows(true)
})
ui.Handle("/sys/kbd/r", func(e ui.Event) {
config.Toggle("sortReversed")
@@ -128,15 +113,15 @@ func Display() bool {
})
ui.Handle("/timer/1s", func(e ui.Event) {
cursor.RefreshContainers()
RedrawRows()
RefreshDisplay()
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
header.Align()
cursor.ScrollPage()
cGrid.SetWidth(ui.TermWidth())
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, maxRows())
RedrawRows()
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
RedrawRows(true)
})
ui.Loop()
@@ -145,7 +130,10 @@ func Display() bool {
return false
}
if expand {
ExpandView(cursor.Selected())
c := cursor.Selected()
if c != nil {
ExpandView(c)
}
return false
}
return true

32
keys.go Normal file
View File

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

82
main.go
View File

@@ -1,6 +1,7 @@
package main
import (
"flag"
"fmt"
"os"
@@ -22,24 +23,65 @@ var (
)
func main() {
readArgs()
defer panicExit()
// init ui
if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
// init global config
config.Init()
// parse command line arguments
var versionFlag = flag.Bool("v", false, "output version information and exit")
var helpFlag = flag.Bool("h", false, "display this help dialog")
var filterFlag = flag.String("f", "", "filter containers")
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
var sortFieldFlag = flag.String("s", "", "select container sort field")
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
var invertFlag = flag.Bool("i", false, "invert default colors")
flag.Parse()
if *versionFlag {
printVersion()
os.Exit(0)
}
if *helpFlag {
printHelp()
os.Exit(0)
}
// override default config values with command line flags
if *filterFlag != "" {
config.Update("filterStr", *filterFlag)
}
if *activeOnlyFlag {
config.Toggle("allContainers")
}
if *sortFieldFlag != "" {
validSort(*sortFieldFlag)
config.Update("sortField", *sortFieldFlag)
}
if *reverseSortFlag {
config.Toggle("sortReversed")
}
// init logger
log = logging.Init()
if config.GetSwitchVal("loggingEnabled") {
logging.StartServer()
}
// init ui
if *invertFlag {
InvertColorMap()
}
ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
// init grid, cursor, header
cursor = NewGridCursor()
cGrid = compact.NewCompactGrid()
@@ -55,24 +97,13 @@ func main() {
}
}
func readArgs() {
if len(os.Args) < 2 {
return
}
for _, arg := range os.Args[1:] {
switch arg {
case "-v", "version":
printVersion()
os.Exit(0)
case "-h", "help":
printHelp()
os.Exit(0)
default:
fmt.Printf("invalid option or argument: \"%s\"\n", arg)
// ensure a given sort field is valid
func validSort(s string) {
if _, ok := Sorters[s]; !ok {
fmt.Printf("invalid sort field: %s\n", s)
os.Exit(1)
}
}
}
func panicExit() {
if r := recover(); r != nil {
@@ -82,19 +113,18 @@ func panicExit() {
}
}
var helpMsg = `cTop - container metric viewer
var helpMsg = `ctop - container metric viewer
usage: ctop [options]
options:
-h display this help dialog
-v output version information and exit
`
func printHelp() {
fmt.Println(helpMsg)
flag.PrintDefaults()
}
func printVersion() {
fmt.Printf("cTop version %v, build %v\n", version, build)
fmt.Printf("ctop version %v, build %v\n", version, build)
}

View File

@@ -11,7 +11,7 @@ var helpDialog = []menu.Item{
menu.Item{"[a] - toggle display of all containers", ""},
menu.Item{"[f] - filter displayed containers", ""},
menu.Item{"[h] - open this help dialog", ""},
menu.Item{"[H] - toggle cTop header", ""},
menu.Item{"[H] - toggle ctop header", ""},
menu.Item{"[s] - select container sort field", ""},
menu.Item{"[r] - reverse container sort order", ""},
menu.Item{"[q] - exit ctop", ""},
@@ -23,9 +23,7 @@ func HelpMenu() {
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.TextFgColor = ui.ColorWhite
m.BorderLabel = "Help"
m.BorderFg = ui.ColorCyan
m.AddItems(helpDialog...)
ui.Render(m)
ui.Handle("/sys/kbd/", func(ui.Event) {
@@ -39,10 +37,9 @@ func FilterMenu() {
defer ui.DefaultEvtStream.ResetHandlers()
i := widgets.NewInput()
i.TextFgColor = ui.ColorWhite
i.BorderLabel = "Filter"
i.BorderFg = ui.ColorCyan
i.SetY(ui.TermHeight() - i.Height)
i.Data = config.GetVal("filterStr")
ui.Render(i)
// refresh container rows on input
@@ -50,13 +47,16 @@ func FilterMenu() {
go func() {
for s := range stream {
config.Update("filterStr", s)
cursor.RefreshContainers()
RedrawRows()
RefreshDisplay()
ui.Render(i)
}
}()
i.InputHandlers()
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
config.Update("filterStr", "")
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("filterStr", i.Data)
ui.StopLoop()
@@ -72,9 +72,7 @@ func SortMenu() {
m := menu.NewMenu()
m.Selectable = true
m.SortItems = true
m.TextFgColor = ui.ColorWhite
m.BorderLabel = "Sort Field"
m.BorderFg = ui.ColorCyan
for _, field := range SortFields() {
m.AddItems(menu.Item{field, ""})
@@ -83,11 +81,15 @@ func SortMenu() {
// set cursor position to current sort field
m.SetCursor(config.GetVal("sortField"))
ui.Render(m)
m.NavigationHandlers()
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("sortField", m.SelectedItem().Val)
ui.StopLoop()
})
ui.Render(m)
ui.Loop()
}

View File

@@ -46,6 +46,7 @@ func (c *Docker) Start() {
c.ReadCPU(s)
c.ReadMem(s)
c.ReadNet(s)
c.ReadIO(s)
c.stream <- c.Metrics
}
log.Infof("collector stopped for container: %s", c.id)
@@ -79,6 +80,7 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
}
func (c *Docker) ReadMem(stats *api.Stats) {
@@ -95,3 +97,16 @@ func (c *Docker) ReadNet(stats *api.Stats) {
}
c.NetRx, c.NetTx = rx, tx
}
func (c *Docker) ReadIO(stats *api.Stats) {
var read, write int64
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
if blk.Op == "Read" {
read = int64(blk.Value)
}
if blk.Op == "Write" {
write = int64(blk.Value)
}
}
c.IOBytesRead, c.IOBytesWrite = read, write
}

View File

@@ -15,6 +15,9 @@ type Metrics struct {
MemLimit int64
MemPercent int
MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
}
func NewMetrics() Metrics {
@@ -24,6 +27,9 @@ func NewMetrics() Metrics {
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
}
}

View File

@@ -13,11 +13,13 @@ type Mock struct {
stream chan Metrics
done bool
running bool
aggression int64
}
func NewMock() *Mock {
func NewMock(a int64) *Mock {
c := &Mock{
Metrics: Metrics{},
aggression: a,
}
c.MemLimit = 2147483648
return c
@@ -47,13 +49,14 @@ func (c *Mock) run() {
defer close(c.stream)
for {
c.CPUUtil += rand.Intn(2)
if c.CPUUtil > 100 {
c.CPUUtil += rand.Intn(2) * int(c.aggression)
if c.CPUUtil >= 100 {
c.CPUUtil = 0
}
c.NetTx += rand.Int63n(600)
c.NetRx += rand.Int63n(600)
c.MemUsage += rand.Int63n(c.MemLimit / 32)
c.NetTx += rand.Int63n(60) * c.aggression
c.NetRx += rand.Int63n(60) * c.aggression
c.MemUsage += rand.Int63n(c.MemLimit/512) * c.aggression
if c.MemUsage > c.MemLimit {
c.MemUsage = 0
}

View File

@@ -26,20 +26,26 @@ func NewMockContainerSource() *MockContainerSource {
// Create Mock containers
func (cs *MockContainerSource) Init() {
total := 20
rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < total; i++ {
//time.Sleep(1 * time.Second)
collector := metrics.NewMock()
for i := 0; i < 4; i++ {
cs.makeContainer(3)
}
for i := 0; i < 16; i++ {
cs.makeContainer(1)
}
}
func (cs *MockContainerSource) makeContainer(aggression int64) {
collector := metrics.NewMock(aggression)
c := NewContainer(makeID(), collector)
c.SetMeta("name", makeName())
c.SetState(makeState())
cs.containers = append(cs.containers, c)
}
}
func (cs *MockContainerSource) Loop() {
iter := 0
for {
@@ -66,6 +72,7 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
// Return array of all containers, sorted by field
func (cs *MockContainerSource) All() Containers {
sort.Sort(cs.containers)
cs.containers.Filter()
return cs.containers
}

28
sort.go
View File

@@ -53,6 +53,22 @@ var Sorters = map[string]sortMethod{
}
return sum1 > sum2
},
"pids": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
if c1.Pids == c2.Pids {
return nameSorter(c1, c2)
}
return c1.Pids > c2.Pids
},
"io": func(c1, c2 *Container) bool {
sum1 := sumIO(c1)
sum2 := sumIO(c2)
// Use secondary sort method if equal values
if sum1 == sum2 {
return nameSorter(c1, c2)
}
return sum1 > sum2
},
"state": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values
c1state := c1.GetMeta("state")
@@ -83,23 +99,23 @@ func (a Containers) Less(i, j int) bool {
return f(a[i], a[j])
}
func (a Containers) Filter() (filtered []*Container) {
func (a Containers) Filter() {
filter := config.GetVal("filterStr")
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a {
c.display = true
// Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil {
continue
c.display = false
}
// Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
continue
c.display = false
}
filtered = append(filtered, c)
}
return filtered
}
func sumNet(c *Container) int64 { return c.NetRx + c.NetTx }
func sumIO(c *Container) int64 { return c.IOBytesRead + c.IOBytesWrite }

View File

@@ -23,10 +23,14 @@ func NewCTopHeader() *CTopHeader {
}
}
func (c *CTopHeader) Render() {
func (c *CTopHeader) Buffer() ui.Buffer {
buf := ui.NewBuffer()
c.Time.Text = timeStr()
ui.Render(c.bg)
ui.Render(c.Time, c.Count, c.Filter)
buf.Merge(c.bg.Buffer())
buf.Merge(c.Time.Buffer())
buf.Merge(c.Count.Buffer())
buf.Merge(c.Filter.Buffer())
return buf
}
func (c *CTopHeader) Align() {
@@ -41,7 +45,7 @@ func headerBgBordered() *ui.Par {
bg := ui.NewPar("")
bg.X = 1
bg.Height = 3
bg.Bg = ui.ColorWhite
bg.Bg = ui.ThemeAttr("header.bg")
return bg
}
@@ -50,7 +54,7 @@ func headerBg() *ui.Par {
bg.X = 1
bg.Height = 1
bg.Border = false
bg.Bg = ui.ColorWhite
bg.Bg = ui.ThemeAttr("header.bg")
return bg
}
@@ -68,7 +72,7 @@ func (c *CTopHeader) SetFilter(val string) {
func timeStr() string {
ts := time.Now().Local().Format("15:04:05 MST")
return fmt.Sprintf("cTop - %s", ts)
return fmt.Sprintf("ctop - %s", ts)
}
func headerPar(x int, s string) *ui.Par {
@@ -77,8 +81,8 @@ func headerPar(x int, s string) *ui.Par {
p.Border = false
p.Height = 1
p.Width = 20
p.TextFgColor = ui.ColorDefault
p.TextBgColor = ui.ColorWhite
p.Bg = ui.ColorWhite
p.Bg = ui.ThemeAttr("header.bg")
p.TextFgColor = ui.ThemeAttr("header.fg")
p.TextBgColor = ui.ThemeAttr("header.bg")
return p
}

View File

@@ -7,7 +7,7 @@ import (
)
var (
input_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
input_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_."
)
type Padding [2]int // x,y padding
@@ -28,10 +28,12 @@ func NewInput() *Input {
Block: *ui.NewBlock(),
Label: "input",
MaxLen: 20,
TextFgColor: ui.ThemeAttr("par.text.fg"),
TextBgColor: ui.ThemeAttr("par.text.bg"),
TextFgColor: ui.ThemeAttr("menu.text.fg"),
TextBgColor: ui.ThemeAttr("menu.text.bg"),
padding: Padding{4, 2},
}
i.BorderFg = ui.ThemeAttr("menu.border.fg")
i.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
i.calcSize()
return i
}

View File

@@ -22,11 +22,13 @@ type Menu struct {
func NewMenu() *Menu {
m := &Menu{
Block: *ui.NewBlock(),
TextFgColor: ui.ThemeAttr("par.text.fg"),
TextBgColor: ui.ThemeAttr("par.text.bg"),
TextFgColor: ui.ThemeAttr("menu.text.fg"),
TextBgColor: ui.ThemeAttr("menu.text.bg"),
cursorPos: 0,
padding: Padding{4, 2},
}
m.BorderFg = ui.ThemeAttr("menu.border.fg")
m.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
m.X = 1
return m
}
@@ -86,7 +88,7 @@ func (m *Menu) Buffer() ui.Buffer {
for _, ch := range item.Text() {
// invert bg/fg colors on currently selected row
if m.Selectable && n == m.cursorPos {
cell = ui.Cell{Ch: ch, Fg: m.TextBgColor, Bg: m.TextFgColor}
cell = ui.Cell{Ch: ch, Fg: ui.ColorBlack, Bg: m.TextFgColor}
} else {
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
}
@@ -98,27 +100,20 @@ func (m *Menu) Buffer() ui.Buffer {
return buf
}
func (m *Menu) Up(ui.Event) {
func (m *Menu) Up() {
if m.cursorPos > 0 {
m.cursorPos--
ui.Render(m)
}
}
func (m *Menu) Down(ui.Event) {
func (m *Menu) Down() {
if m.cursorPos < (len(m.items) - 1) {
m.cursorPos++
ui.Render(m)
}
}
// Setup some default handlers for menu navigation
func (m *Menu) NavigationHandlers() {
ui.Handle("/sys/kbd/<up>", m.Up)
ui.Handle("/sys/kbd/<down>", m.Down)
ui.Handle("/sys/kbd/q", func(ui.Event) { ui.StopLoop() })
}
// Set width and height based on menu items
func (m *Menu) calcSize() {
m.Width = 7 // minimum width