Compare commits

..

41 Commits
v0.3 ... v0.4.1

Author SHA1 Message Date
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
32 changed files with 324 additions and 150 deletions

7
Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM quay.io/vektorcloud/glibc:latest
RUN ctop_url=$(wget -q -O - https://api.github.com/repos/bcicen/ctop/releases/latest | grep 'browser_' | cut -d\" -f4 |grep 'linux-amd64') && \
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.4.1/ctop-0.4.1-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,14 +25,21 @@ 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.4.1/ctop-0.4.1-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 -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/)
## 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
@@ -36,8 +51,10 @@ 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
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.4.1

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

24
circle.yml Normal file
View File

@@ -0,0 +1,24 @@
machine:
services:
- docker
dependencies:
override:
- docker info
- |
if [[ "$CIRCLE_BRANCH" == "master" ]]; then
docker build -t quay.io/vektorlab/ctop:latest .
else
docker build -t quay.io/vektorlab/ctop:${CIRCLE_BRANCH} .
fi
test:
override:
- docker run -t --entrypoint /bin/sh quay.io/vektorlab/ctop:latest -v
deployment:
hub:
branch: master
commands:
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
- docker push quay.io/vektorlab/ctop:latest

43
colors.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import 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,
"sparkline.line.fg": ui.ColorGreen,
"sparkline.title.fg": 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

@@ -12,7 +12,7 @@ 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 {
@@ -20,28 +20,34 @@ func NewCompactGrid() *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

@@ -18,7 +18,7 @@ func (row *Compact) SetCPU(val int) {
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")
} }
row.Cpu.Percent = val row.Cpu.Percent = val
} }
@@ -29,7 +29,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.ColorBlack
w.TextBgColor = ui.ThemeAttr("par.text.fg") w.TextBgColor = ui.ThemeAttr("par.text.fg")
} }

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

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
} }

66
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)
@@ -63,7 +50,7 @@ func ExpandView(c *Container) {
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.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop() ui.StopLoop()
@@ -73,6 +60,11 @@ func ExpandView(c *Container) {
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,21 @@ 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) { ui.Handle("/sys/kbd/<up>", func(ui.Event) { cursor.Up() })
cursor.Up() ui.Handle("/sys/kbd/<down>", func(ui.Event) { cursor.Down() })
})
ui.Handle("/sys/kbd/<down>", func(ui.Event) {
cursor.Down()
})
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/q", func(ui.Event) { ui.StopLoop() })
ui.Handle("/sys/kbd/C-c", func(ui.Event) { 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())
@@ -114,10 +104,7 @@ func Display() bool {
}) })
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 +115,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 +132,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

View File

@@ -26,6 +26,7 @@ func main() {
defer panicExit() defer panicExit()
// init ui // init ui
ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
panic(err) panic(err)
} }
@@ -82,7 +83,7 @@ func panicExit() {
} }
} }
var helpMsg = `cTop - container metric viewer var helpMsg = `ctop - container metric viewer
usage: ctop [options] usage: ctop [options]
@@ -96,5 +97,5 @@ func printHelp() {
} }
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,9 +37,7 @@ 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)
ui.Render(i) ui.Render(i)
@@ -50,8 +46,7 @@ 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)
} }
}() }()
@@ -72,9 +67,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, ""})

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
} }

10
sort.go
View File

@@ -83,23 +83,21 @@ 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 }

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

@@ -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}
} }