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 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 ## Install
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform: 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 #### Linux
```bash ```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 mv ctop /usr/local/bin/
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
@@ -17,27 +25,59 @@ sudo chmod +x /usr/local/bin/ctop
#### OS X #### OS X
```bash ```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 mv ctop /usr/local/bin/
sudo chmod +x /usr/local/bin/ctop 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 ## 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 ```bash
export DOCKER_HOST=tcp://127.0.0.1:4243 export DOCKER_HOST=tcp://127.0.0.1:4243
ctop 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 ### Keybindings
Key | Action Key | Action
--- | --- --- | ---
a | Toggle display of all (running and non-running) containers a | Toggle display of all (running and non-running) containers
f | Filter displayed containers f | Filter displayed containers (`esc` to clear when open)
H | Toggle cTop header H | Toggle ctop header
h | Open help dialog h | Open help dialog
s | Select container sort field s | Select container sort field
r | Reverse container sort order 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 // defaults
var params = []*Param{ var params = []*Param{
&Param{
Key: "dockerHost",
Val: getEnv("DOCKER_HOST", "unix:///var/run/docker.sock"),
Label: "Docker API URL",
},
&Param{ &Param{
Key: "filterStr", Key: "filterStr",
Val: "", Val: "",

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import (
type GridCursor struct { type GridCursor struct {
selectedID string // id of currently selected container selectedID string // id of currently selected container
containers Containers filtered Containers
cSource ContainerSource cSource ContainerSource
} }
@@ -16,64 +16,117 @@ func NewGridCursor() *GridCursor {
} }
} }
func (gc *GridCursor) Len() int { return len(gc.containers) } func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) Selected() *Container { return gc.containers[gc.Idx()] }
func (gc *GridCursor) RefreshContainers() { func (gc *GridCursor) Selected() *Container {
gc.containers = gc.cSource.All().Filter() 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 == "" { if gc.selectedID == "" {
gc.Reset() gc.Reset()
} }
return lenChanged
} }
// Set an initial cursor position, if possible // Set an initial cursor position, if possible
func (gc *GridCursor) Reset() { func (gc *GridCursor) Reset() {
for _, c := range gc.cSource.All() {
c.Widgets.Name.UnHighlight()
}
if gc.Len() > 0 { if gc.Len() > 0 {
gc.selectedID = gc.containers[0].Id gc.selectedID = gc.filtered[0].Id
gc.containers[0].Widgets.Name.Highlight() gc.filtered[0].Widgets.Name.Highlight()
} }
} }
// Return current cursor index // Return current cursor index
func (gc *GridCursor) Idx() int { func (gc *GridCursor) Idx() int {
for n, c := range gc.containers { for n, c := range gc.filtered {
if c.Id == gc.selectedID { if c.Id == gc.selectedID {
return n return n
} }
} }
gc.Reset()
return 0 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() { func (gc *GridCursor) Up() {
idx := gc.Idx() idx := gc.Idx()
// decrement if possible if idx <= 0 { // already at top
if idx <= 0 {
return return
} }
active := gc.containers[idx] active := gc.filtered[idx]
next := gc.containers[idx-1] next := gc.filtered[idx-1]
active.Widgets.Name.UnHighlight() active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id gc.selectedID = next.Id
next.Widgets.Name.Highlight() next.Widgets.Name.Highlight()
gc.ScrollPage()
ui.Render(cGrid) ui.Render(cGrid)
} }
func (gc *GridCursor) Down() { func (gc *GridCursor) Down() {
idx := gc.Idx() idx := gc.Idx()
// increment if possible if idx >= gc.Len()-1 { // already at bottom
if idx >= (gc.Len() - 1) {
return return
} }
if idx >= maxRows()-1 { active := gc.filtered[idx]
return next := gc.filtered[idx+1]
}
active := gc.containers[idx]
next := gc.containers[idx+1]
active.Widgets.Name.UnHighlight() active.Widgets.Name.UnHighlight()
gc.selectedID = next.Id gc.selectedID = next.Id
next.Widgets.Name.Highlight() next.Widgets.Name.Highlight()
gc.ScrollPage()
ui.Render(cGrid) ui.Render(cGrid)
} }

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ type CompactHeader struct {
} }
func NewCompactHeader() *CompactHeader { 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 := &CompactHeader{}
ch.Height = 2 ch.Height = 2
for _, f := range fields { for _, f := range fields {
@@ -27,13 +27,13 @@ func (ch *CompactHeader) GetHeight() int {
func (ch *CompactHeader) SetWidth(w int) { func (ch *CompactHeader) SetWidth(w int) {
x := ch.X x := ch.X
autoWidth := calcWidth(w, 5) autoWidth := calcWidth(w)
for n, col := range ch.pars { for n, col := range ch.pars {
// set status column to static width // set column to static width
if n == 0 { if colWidths[n] != 0 {
col.SetX(x) col.SetX(x)
col.SetWidth(statusWidth) col.SetWidth(colWidths[n])
x += statusWidth x += colWidths[n]
continue continue
} }
col.SetX(x) col.SetX(x)

View File

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

View File

@@ -13,12 +13,25 @@ func (row *Compact) SetNet(rx int64, tx int64) {
row.Net.Set(label) 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) { func (row *Compact) SetCPU(val int) {
row.Cpu.BarColor = colorScale(val) row.Cpu.BarColor = colorScale(val)
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val)) row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
if val < 5 { if val < 5 {
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 row.Cpu.Percent = val
} }
@@ -29,7 +42,7 @@ func (row *Compact) SetMem(val int64, limit int64, percent int) {
percent = 5 percent = 5
row.Memory.BarColor = ui.ColorBlack row.Memory.BarColor = ui.ColorBlack
} else { } else {
row.Memory.BarColor = ui.ColorGreen row.Memory.BarColor = ui.ThemeAttr("gauge.bar.bg")
} }
row.Memory.Percent = percent row.Memory.Percent = percent
} }

View File

@@ -17,7 +17,7 @@ func NewTextCol(s string) *TextCol {
} }
func (w *TextCol) Highlight() { 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") w.TextBgColor = ui.ThemeAttr("par.text.fg")
} }

View File

@@ -9,10 +9,29 @@ import (
const colSpacing = 1 const colSpacing = 1
// Calculate per-column width, given total width and number of items // per-column width. 0 == auto width
func calcWidth(width, items int) int { var colWidths = []int{
spacing := colSpacing * items 3, // status
return (width - statusWidth - spacing) / items 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) { func centerParText(p *ui.Par) {

View File

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

View File

@@ -15,8 +15,8 @@ func NewInfo(id string) *Info {
p := ui.NewTable() p := ui.NewTable()
p.Height = 4 p.Height = 4
p.Width = colWidth[0] p.Width = colWidth[0]
p.FgColor = ui.ColorWhite p.FgColor = ui.ThemeAttr("par.text.fg")
p.Seperator = false p.Separator = false
i := &Info{p, make(map[string]string)} i := &Info{p, make(map[string]string)}
i.Set("id", id) i.Set("id", id)
return i 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 Net *Net
Cpu *Cpu Cpu *Cpu
Mem *Mem Mem *Mem
IO *IO
X, Y int
Width int Width int
} }
@@ -29,30 +31,59 @@ func NewExpanded(id string) *Expanded {
Net: NewNet(), Net: NewNet(),
Cpu: NewCpu(), Cpu: NewCpu(),
Mem: NewMem(), Mem: NewMem(),
IO: NewIO(),
Width: ui.TermWidth(), Width: ui.TermWidth(),
} }
} }
func (e *Expanded) SetWidth(w int) { func (e *Expanded) Up() {
e.Width = w if e.Y < 0 {
e.Y++
e.Align()
ui.Render(e)
}
} }
func (e *Expanded) SetMeta(k, v string) { func (e *Expanded) Down() {
e.Info.Set(k, v) 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) { func (e *Expanded) SetMetrics(m metrics.Metrics) {
e.Cpu.Update(m.CPUUtil) e.Cpu.Update(m.CPUUtil)
e.Net.Update(m.NetRx, m.NetTx) e.Net.Update(m.NetRx, m.NetTx)
e.Mem.Update(int(m.MemUsage), int(m.MemLimit)) 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() { 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() { for _, i := range e.all() {
i.SetY(y) i.SetY(y)
y += i.GetHeight() y += i.GetHeight()
} }
if e.Width > colWidth[0] { if e.Width > colWidth[0] {
colWidth[1] = e.Width - (colWidth[0] + 1) 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.Cpu.Buffer())
buf.Merge(e.Mem.Buffer()) buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer()) buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
return buf return buf
} }
@@ -83,6 +115,7 @@ func (e *Expanded) all() []ui.GridBufferer {
e.Cpu, e.Cpu,
e.Mem, e.Mem,
e.Net, e.Net,
e.IO,
} }
} }

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ package main
import ( import (
"sort" "sort"
"strings" "strings"
"sync"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/metrics"
"github.com/fsouza/go-dockerclient" "github.com/fsouza/go-dockerclient"
) )
@@ -18,11 +18,12 @@ type DockerContainerSource struct {
client *docker.Client client *docker.Client
containers map[string]*Container containers map[string]*Container
needsRefresh chan string // container IDs requiring refresh needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
} }
func NewDockerContainerSource() *DockerContainerSource { func NewDockerContainerSource() *DockerContainerSource {
// init docker client // init docker client
client, err := docker.NewClient(config.GetVal("dockerHost")) client, err := docker.NewClientFromEnv()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -30,9 +31,10 @@ func NewDockerContainerSource() *DockerContainerSource {
client: client, client: client,
containers: make(map[string]*Container), containers: make(map[string]*Container),
needsRefresh: make(chan string, 60), needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
} }
cm.refreshAll()
go cm.Loop() go cm.Loop()
cm.refreshAll()
go cm.watchEvents() go cm.watchEvents()
return cm return cm
} }
@@ -113,29 +115,38 @@ func (cm *DockerContainerSource) MustGet(id string) *Container {
collector := metrics.NewDocker(cm.client, id) collector := metrics.NewDocker(cm.client, id)
// create container // create container
c = NewContainer(id, collector) c = NewContainer(id, collector)
cm.lock.Lock()
cm.containers[id] = c cm.containers[id] = c
cm.lock.Unlock()
} }
return c return c
} }
// Get a single container, by ID // Get a single container, by ID
func (cm *DockerContainerSource) Get(id string) (*Container, bool) { func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id] c, ok := cm.containers[id]
cm.lock.Unlock()
return c, ok return c, ok
} }
// Remove containers by ID // Remove containers by ID
func (cm *DockerContainerSource) delByID(id string) { func (cm *DockerContainerSource) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id) delete(cm.containers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id) log.Infof("removed dead container: %s", id)
} }
// Return array of all containers, sorted by field // Return array of all containers, sorted by field
func (cm *DockerContainerSource) All() (containers Containers) { func (cm *DockerContainerSource) All() (containers Containers) {
cm.lock.Lock()
for _, c := range cm.containers { for _, c := range cm.containers {
containers = append(containers, c) containers = append(containers, c)
} }
cm.lock.Unlock()
sort.Sort(containers) sort.Sort(containers)
containers.Filter()
return containers 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" ui "github.com/gizak/termui"
) )
func maxRows() int { func RedrawRows(clr bool) {
return ui.TermHeight() - 2 - cGrid.Y
}
func RedrawRows() {
// reinit body rows // reinit body rows
cGrid.Clear() cGrid.Clear()
@@ -23,25 +19,16 @@ func RedrawRows() {
} }
cGrid.SetY(y) cGrid.SetY(y)
var cursorVisible bool for _, c := range cursor.filtered {
max := maxRows()
for n, c := range cursor.containers {
if n >= max {
break
}
cGrid.AddRows(c.Widgets) cGrid.AddRows(c.Widgets)
if c.Id == cursor.selectedID {
cursorVisible = true
}
}
if !cursorVisible {
cursor.Reset()
} }
if clr {
ui.Clear() ui.Clear()
log.Debugf("screen cleared")
}
if config.GetSwitchVal("enableHeader") { if config.GetSwitchVal("enableHeader") {
header.Render() ui.Render(header)
} }
cGrid.Align() cGrid.Align()
ui.Render(cGrid) ui.Render(cGrid)
@@ -57,22 +44,27 @@ func ExpandView(c *Container) {
ex.Align() ex.Align()
ui.Render(ex) 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) { ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ex.SetWidth(ui.TermWidth()) ex.SetWidth(ui.TermWidth())
ex.Align() 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) c.SetUpdater(c.Widgets)
} }
func RefreshDisplay() {
needsClear := cursor.RefreshContainers()
RedrawRows(needsClear)
}
func Display() bool { func Display() bool {
var menu func() var menu func()
var expand bool var expand bool
@@ -83,23 +75,23 @@ func Display() bool {
// initial draw // initial draw
header.Align() header.Align()
cursor.RefreshContainers() cursor.RefreshContainers()
RedrawRows() RedrawRows(true)
ui.Handle("/sys/kbd/<up>", func(ui.Event) { HandleKeys("up", cursor.Up)
cursor.Up() HandleKeys("down", cursor.Down)
}) HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<down>", func(ui.Event) { HandleKeys("help", func() {
cursor.Down() menu = HelpMenu
ui.StopLoop()
}) })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
expand = true expand = true
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/a", func(ui.Event) { ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers") config.Toggle("allContainers")
cursor.RefreshContainers() RefreshDisplay()
RedrawRows()
}) })
ui.Handle("/sys/kbd/D", func(ui.Event) { ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected()) dumpContainer(cursor.Selected())
@@ -108,16 +100,9 @@ func Display() bool {
menu = FilterMenu menu = FilterMenu
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/h", func(ui.Event) {
menu = HelpMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/H", func(ui.Event) { ui.Handle("/sys/kbd/H", func(ui.Event) {
config.Toggle("enableHeader") config.Toggle("enableHeader")
RedrawRows() RedrawRows(true)
})
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.StopLoop()
}) })
ui.Handle("/sys/kbd/r", func(e ui.Event) { ui.Handle("/sys/kbd/r", func(e ui.Event) {
config.Toggle("sortReversed") config.Toggle("sortReversed")
@@ -128,15 +113,15 @@ func Display() bool {
}) })
ui.Handle("/timer/1s", func(e ui.Event) { ui.Handle("/timer/1s", func(e ui.Event) {
cursor.RefreshContainers() RefreshDisplay()
RedrawRows()
}) })
ui.Handle("/sys/wnd/resize", func(e ui.Event) { ui.Handle("/sys/wnd/resize", func(e ui.Event) {
header.Align() header.Align()
cursor.ScrollPage()
cGrid.SetWidth(ui.TermWidth()) cGrid.SetWidth(ui.TermWidth())
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, maxRows()) log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
RedrawRows() RedrawRows(true)
}) })
ui.Loop() ui.Loop()
@@ -145,7 +130,10 @@ func Display() bool {
return false return false
} }
if expand { if expand {
ExpandView(cursor.Selected()) c := cursor.Selected()
if c != nil {
ExpandView(c)
}
return false return false
} }
return true 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 package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
@@ -22,24 +23,65 @@ var (
) )
func main() { func main() {
readArgs()
defer panicExit() defer panicExit()
// init ui
if err := ui.Init(); err != nil {
panic(err)
}
defer ui.Close()
// init global config // init global config
config.Init() 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 // init logger
log = logging.Init() log = logging.Init()
if config.GetSwitchVal("loggingEnabled") { if config.GetSwitchVal("loggingEnabled") {
logging.StartServer() 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 // init grid, cursor, header
cursor = NewGridCursor() cursor = NewGridCursor()
cGrid = compact.NewCompactGrid() cGrid = compact.NewCompactGrid()
@@ -55,24 +97,13 @@ func main() {
} }
} }
func readArgs() { // ensure a given sort field is valid
if len(os.Args) < 2 { func validSort(s string) {
return if _, ok := Sorters[s]; !ok {
} fmt.Printf("invalid sort field: %s\n", s)
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)
os.Exit(1) os.Exit(1)
} }
} }
}
func panicExit() { func panicExit() {
if r := recover(); r != nil { 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] usage: ctop [options]
options: options:
-h display this help dialog
-v output version information and exit
` `
func printHelp() { func printHelp() {
fmt.Println(helpMsg) fmt.Println(helpMsg)
flag.PrintDefaults()
} }
func printVersion() { 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{"[a] - toggle display of all containers", ""},
menu.Item{"[f] - filter displayed containers", ""}, menu.Item{"[f] - filter displayed containers", ""},
menu.Item{"[h] - open this help dialog", ""}, 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{"[s] - select container sort field", ""},
menu.Item{"[r] - reverse container sort order", ""}, menu.Item{"[r] - reverse container sort order", ""},
menu.Item{"[q] - exit ctop", ""}, menu.Item{"[q] - exit ctop", ""},
@@ -23,9 +23,7 @@ func HelpMenu() {
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu() m := menu.NewMenu()
m.TextFgColor = ui.ColorWhite
m.BorderLabel = "Help" m.BorderLabel = "Help"
m.BorderFg = ui.ColorCyan
m.AddItems(helpDialog...) m.AddItems(helpDialog...)
ui.Render(m) ui.Render(m)
ui.Handle("/sys/kbd/", func(ui.Event) { ui.Handle("/sys/kbd/", func(ui.Event) {
@@ -39,10 +37,9 @@ func FilterMenu() {
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
i := widgets.NewInput() i := widgets.NewInput()
i.TextFgColor = ui.ColorWhite
i.BorderLabel = "Filter" i.BorderLabel = "Filter"
i.BorderFg = ui.ColorCyan
i.SetY(ui.TermHeight() - i.Height) i.SetY(ui.TermHeight() - i.Height)
i.Data = config.GetVal("filterStr")
ui.Render(i) ui.Render(i)
// refresh container rows on input // refresh container rows on input
@@ -50,13 +47,16 @@ func FilterMenu() {
go func() { go func() {
for s := range stream { for s := range stream {
config.Update("filterStr", s) config.Update("filterStr", s)
cursor.RefreshContainers() RefreshDisplay()
RedrawRows()
ui.Render(i) ui.Render(i)
} }
}() }()
i.InputHandlers() i.InputHandlers()
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
config.Update("filterStr", "")
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("filterStr", i.Data) config.Update("filterStr", i.Data)
ui.StopLoop() ui.StopLoop()
@@ -72,9 +72,7 @@ func SortMenu() {
m := menu.NewMenu() m := menu.NewMenu()
m.Selectable = true m.Selectable = true
m.SortItems = true m.SortItems = true
m.TextFgColor = ui.ColorWhite
m.BorderLabel = "Sort Field" m.BorderLabel = "Sort Field"
m.BorderFg = ui.ColorCyan
for _, field := range SortFields() { for _, field := range SortFields() {
m.AddItems(menu.Item{field, ""}) m.AddItems(menu.Item{field, ""})
@@ -83,11 +81,15 @@ func SortMenu() {
// set cursor position to current sort field // set cursor position to current sort field
m.SetCursor(config.GetVal("sortField")) m.SetCursor(config.GetVal("sortField"))
ui.Render(m) HandleKeys("up", m.Up)
m.NavigationHandlers() HandleKeys("down", m.Down)
HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("sortField", m.SelectedItem().Val) config.Update("sortField", m.SelectedItem().Val)
ui.StopLoop() ui.StopLoop()
}) })
ui.Render(m)
ui.Loop() ui.Loop()
} }

View File

@@ -46,6 +46,7 @@ func (c *Docker) Start() {
c.ReadCPU(s) c.ReadCPU(s)
c.ReadMem(s) c.ReadMem(s)
c.ReadNet(s) c.ReadNet(s)
c.ReadIO(s)
c.stream <- c.Metrics c.stream <- c.Metrics
} }
log.Infof("collector stopped for container: %s", c.id) 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.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
c.lastCpu = total c.lastCpu = total
c.lastSysCpu = system c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
} }
func (c *Docker) ReadMem(stats *api.Stats) { func (c *Docker) ReadMem(stats *api.Stats) {
@@ -95,3 +97,16 @@ func (c *Docker) ReadNet(stats *api.Stats) {
} }
c.NetRx, c.NetTx = rx, tx 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 MemLimit int64
MemPercent int MemPercent int
MemUsage int64 MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
} }
func NewMetrics() Metrics { func NewMetrics() Metrics {
@@ -24,6 +27,9 @@ func NewMetrics() Metrics {
NetRx: -1, NetRx: -1,
MemUsage: -1, MemUsage: -1,
MemPercent: -1, MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
} }
} }

View File

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

View File

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

28
sort.go
View File

@@ -53,6 +53,22 @@ var Sorters = map[string]sortMethod{
} }
return sum1 > sum2 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 { "state": func(c1, c2 *Container) bool {
// Use secondary sort method if equal values // Use secondary sort method if equal values
c1state := c1.GetMeta("state") c1state := c1.GetMeta("state")
@@ -83,23 +99,23 @@ func (a Containers) Less(i, j int) bool {
return f(a[i], a[j]) return f(a[i], a[j])
} }
func (a Containers) Filter() (filtered []*Container) { func (a Containers) Filter() {
filter := config.GetVal("filterStr") filter := config.GetVal("filterStr")
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter)) re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a { for _, c := range a {
c.display = true
// Apply name filter // Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil { if re.FindAllString(c.GetMeta("name"), 1) == nil {
continue c.display = false
} }
// Apply state filter // Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" { 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 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() c.Time.Text = timeStr()
ui.Render(c.bg) buf.Merge(c.bg.Buffer())
ui.Render(c.Time, c.Count, c.Filter) buf.Merge(c.Time.Buffer())
buf.Merge(c.Count.Buffer())
buf.Merge(c.Filter.Buffer())
return buf
} }
func (c *CTopHeader) Align() { func (c *CTopHeader) Align() {
@@ -41,7 +45,7 @@ func headerBgBordered() *ui.Par {
bg := ui.NewPar("") bg := ui.NewPar("")
bg.X = 1 bg.X = 1
bg.Height = 3 bg.Height = 3
bg.Bg = ui.ColorWhite bg.Bg = ui.ThemeAttr("header.bg")
return bg return bg
} }
@@ -50,7 +54,7 @@ func headerBg() *ui.Par {
bg.X = 1 bg.X = 1
bg.Height = 1 bg.Height = 1
bg.Border = false bg.Border = false
bg.Bg = ui.ColorWhite bg.Bg = ui.ThemeAttr("header.bg")
return bg return bg
} }
@@ -68,7 +72,7 @@ func (c *CTopHeader) SetFilter(val string) {
func timeStr() string { func timeStr() string {
ts := time.Now().Local().Format("15:04:05 MST") 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 { func headerPar(x int, s string) *ui.Par {
@@ -77,8 +81,8 @@ func headerPar(x int, s string) *ui.Par {
p.Border = false p.Border = false
p.Height = 1 p.Height = 1
p.Width = 20 p.Width = 20
p.TextFgColor = ui.ColorDefault p.Bg = ui.ThemeAttr("header.bg")
p.TextBgColor = ui.ColorWhite p.TextFgColor = ui.ThemeAttr("header.fg")
p.Bg = ui.ColorWhite p.TextBgColor = ui.ThemeAttr("header.bg")
return p return p
} }

View File

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

View File

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