From 1271ce96e8517ea89f128ef41a860cec2438497e Mon Sep 17 00:00:00 2001 From: Marcos Diez Date: Thu, 6 Jun 2019 09:58:55 -0300 Subject: [PATCH 01/16] shows total memory usage --- cursor.go | 10 ++++++++++ grid.go | 1 + widgets/header.go | 9 ++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cursor.go b/cursor.go index 6c5a6e8..e518531 100644 --- a/cursor.go +++ b/cursor.go @@ -17,6 +17,16 @@ type GridCursor struct { func (gc *GridCursor) Len() int { return len(gc.filtered) } +func (gc *GridCursor) MemoryUsage() int64 { + var size int64 + size = 0 + for _, c := range gc.filtered { + size += c.MemUsage + } + gc.Reset() + return size +} + func (gc *GridCursor) Selected() *container.Container { idx := gc.Idx() if idx < gc.Len() { diff --git a/grid.go b/grid.go index 655e000..d370e1d 100644 --- a/grid.go +++ b/grid.go @@ -52,6 +52,7 @@ func RedrawRows(clr bool) { y := 1 if config.GetSwitchVal("enableHeader") { header.SetCount(cursor.Len()) + header.SetMemoryUsage(cursor.MemoryUsage()) header.SetFilter(config.GetVal("filterStr")) y += header.Height() } diff --git a/widgets/header.go b/widgets/header.go index a7ab786..e607b84 100644 --- a/widgets/header.go +++ b/widgets/header.go @@ -3,7 +3,7 @@ package widgets import ( "fmt" "time" - + "github.com/bcicen/ctop/cwidgets" ui "github.com/gizak/termui" ) @@ -11,6 +11,7 @@ type CTopHeader struct { Time *ui.Par Count *ui.Par Filter *ui.Par + Mem *ui.Par bg *ui.Par } @@ -19,6 +20,7 @@ func NewCTopHeader() *CTopHeader { Time: headerPar(2, ""), Count: headerPar(24, "-"), Filter: headerPar(40, ""), + Mem: headerPar(70, ""), bg: headerBg(), } } @@ -30,6 +32,7 @@ func (c *CTopHeader) Buffer() ui.Buffer { buf.Merge(c.Time.Buffer()) buf.Merge(c.Count.Buffer()) buf.Merge(c.Filter.Buffer()) + buf.Merge(c.Mem.Buffer()) return buf } @@ -58,6 +61,10 @@ func headerBg() *ui.Par { return bg } +func (c *CTopHeader) SetMemoryUsage(val int64) { + c.Mem.Text = cwidgets.ByteFormat(val) +} + func (c *CTopHeader) SetCount(val int) { c.Count.Text = fmt.Sprintf("%d containers", val) } From 923edb967e45763758a4c16ff3a2eef2f549342e Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Sat, 8 Jun 2019 21:34:43 +0000 Subject: [PATCH 02/16] initial refactor of all column widgets to standard interface --- container/main.go | 15 ++-- cwidgets/compact/gauge.go | 55 ++++++++++++- cwidgets/compact/main.go | 149 +++++++++++++----------------------- cwidgets/compact/setters.go | 48 ------------ cwidgets/compact/status.go | 26 +++++-- cwidgets/compact/text.go | 58 ++++++++++++-- cwidgets/main.go | 2 +- cwidgets/single/main.go | 12 +-- models/main.go | 11 +++ 9 files changed, 198 insertions(+), 178 deletions(-) delete mode 100644 cwidgets/compact/setters.go diff --git a/container/main.go b/container/main.go index 416b878..660e5cc 100644 --- a/container/main.go +++ b/container/main.go @@ -21,7 +21,7 @@ const ( type Container struct { models.Metrics Id string - Meta map[string]string + Meta models.Meta Widgets *compact.Compact Display bool // display this container in compact view updater cwidgets.WidgetUpdater @@ -34,7 +34,7 @@ func New(id string, collector collector.Collector, manager manager.Manager) *Con return &Container{ Metrics: models.NewMetrics(), Id: id, - Meta: make(map[string]string), + Meta: models.NewMeta(), Widgets: widgets, updater: widgets, collector: collector, @@ -44,21 +44,16 @@ func New(id string, collector collector.Collector, manager manager.Manager) *Con func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) { c.updater = u - for k, v := range c.Meta { - c.updater.SetMeta(k, v) - } + c.updater.SetMeta(c.Meta) } func (c *Container) SetMeta(k, v string) { c.Meta[k] = v - c.updater.SetMeta(k, v) + c.updater.SetMeta(c.Meta) } func (c *Container) GetMeta(k string) string { - if v, ok := c.Meta[k]; ok { - return v - } - return "" + return c.Meta.Get(k) } func (c *Container) SetState(s string) { diff --git a/cwidgets/compact/gauge.go b/cwidgets/compact/gauge.go index 481fb32..b22042f 100644 --- a/cwidgets/compact/gauge.go +++ b/cwidgets/compact/gauge.go @@ -1,21 +1,49 @@ package compact import ( + "fmt" + + "github.com/bcicen/ctop/cwidgets" + "github.com/bcicen/ctop/models" ui "github.com/gizak/termui" ) +type CPUCol struct { + *GaugeCol +} + +func (w *CPUCol) SetMetrics(m models.Metrics) { + val := m.CPUUtil + w.BarColor = colorScale(val) + w.Label = fmt.Sprintf("%d%%", val) + + if val > 100 { + val = 100 + } + w.Percent = val +} + +type MemCol struct { + *GaugeCol +} + +func (w *MemCol) SetMetrics(m models.Metrics) { + w.BarColor = ui.ThemeAttr("gauge.bar.bg") + w.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(m.MemUsage), cwidgets.ByteFormat(m.MemLimit)) + w.Percent = m.MemPercent +} + type GaugeCol struct { *ui.Gauge } func NewGaugeCol() *GaugeCol { - g := ui.NewGauge() + g := &GaugeCol{ui.NewGauge()} g.Height = 1 g.Border = false - g.Percent = 0 g.PaddingBottom = 0 - g.Label = "-" - return &GaugeCol{g} + g.Reset() + return g } func (w *GaugeCol) Reset() { @@ -23,11 +51,30 @@ func (w *GaugeCol) Reset() { w.Percent = 0 } +func (w *GaugeCol) Buffer() ui.Buffer { + // if bar would not otherwise be visible, set a minimum + // percentage value and low-contrast color for structure + if w.Percent < 5 { + w.Percent = 5 + w.BarColor = ui.ColorBlack + } + + return w.Gauge.Buffer() +} + +// GaugeCol implements CompactCol +func (w *GaugeCol) SetMeta(models.Meta) {} + +// GaugeCol implements CompactCol +func (w *GaugeCol) SetMetrics(models.Metrics) {} + +// GaugeCol implements CompactCol func (w *GaugeCol) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") w.PercentColor = ui.ThemeAttr("par.text.hi") } +// GaugeCol implements CompactCol func (w *GaugeCol) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") w.PercentColor = ui.ThemeAttr("par.text.bg") diff --git a/cwidgets/compact/main.go b/cwidgets/compact/main.go index af948fc..9f6cef4 100644 --- a/cwidgets/compact/main.go +++ b/cwidgets/compact/main.go @@ -9,16 +9,18 @@ import ( var log = logging.Init() +type CompactCol interface { + ui.GridBufferer + Reset() + Highlight() + UnHighlight() + SetMeta(models.Meta) + SetMetrics(models.Metrics) +} + type Compact struct { - Status *Status - Name *TextCol - Cid *TextCol - Cpu *GaugeCol - Mem *GaugeCol - Net *TextCol - IO *TextCol - Pids *TextCol Bg *RowBg + Cols []CompactCol X, Y int Width int Height int @@ -30,64 +32,44 @@ func NewCompact(id string) *Compact { id = id[:12] } row := &Compact{ - Status: NewStatus(), - Name: NewTextCol("-"), - Cid: NewTextCol(id), - Cpu: NewGaugeCol(), - Mem: NewGaugeCol(), - Net: NewTextCol("-"), - IO: NewTextCol("-"), - Pids: NewTextCol("-"), - Bg: NewRowBg(), + Bg: NewRowBg(), + Cols: []CompactCol{ + NewStatus(), + &NameCol{NewTextCol("-")}, + &CIDCol{NewTextCol(id)}, + &CPUCol{NewGaugeCol()}, + &MemCol{NewGaugeCol()}, + &NetCol{NewTextCol("-")}, + &IOCol{NewTextCol("-")}, + &PIDCol{NewTextCol("-")}, + }, X: 1, Height: 1, } return row } -//func (row *Compact) ToggleExpand() { -//if row.Height == 1 { -//row.Height = 4 -//} else { -//row.Height = 1 -//} -//} - -func (row *Compact) SetMeta(k, v string) { - switch k { - case "name": - row.Name.Set(v) - case "state": - row.Status.Set(v) - case "health": - row.Status.SetHealth(v) +func (row *Compact) SetMeta(m models.Meta) { + for _, w := range row.Cols { + w.SetMeta(m) } } func (row *Compact) SetMetrics(m models.Metrics) { - row.SetCPU(m.CPUUtil) - row.SetNet(m.NetRx, m.NetTx) - row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent) - row.SetIO(m.IOBytesRead, m.IOBytesWrite) - row.SetPids(m.Pids) + for _, w := range row.Cols { + w.SetMetrics(m) + } } -// Set gauges, counters to default unread values +// Set gauges, counters, etc. to default unread values func (row *Compact) Reset() { - row.Cpu.Reset() - row.Mem.Reset() - row.Net.Reset() - row.IO.Reset() - row.Pids.Reset() + for _, w := range row.Cols { + w.Reset() + } } -func (row *Compact) GetHeight() int { - return row.Height -} - -func (row *Compact) SetX(x int) { - row.X = x -} +func (row *Compact) GetHeight() int { return row.Height } +func (row *Compact) SetX(x int) { row.X = x } func (row *Compact) SetY(y int) { if y == row.Y { @@ -95,8 +77,8 @@ func (row *Compact) SetY(y int) { } row.Bg.Y = y - for _, col := range row.all() { - col.SetY(y) + for _, w := range row.Cols { + w.SetY(y) } row.Y = y } @@ -111,15 +93,17 @@ func (row *Compact) SetWidth(width int) { row.Bg.SetWidth(width) autoWidth := calcWidth(width) - for n, col := range row.all() { + for n, w := range row.Cols { + // set static width, if provided if colWidths[n] != 0 { - col.SetX(x) - col.SetWidth(colWidths[n]) + w.SetX(x) + w.SetWidth(colWidths[n]) x += colWidths[n] continue } - col.SetX(x) - col.SetWidth(autoWidth) + // else use auto width + w.SetX(x) + w.SetWidth(autoWidth) x += autoWidth + colSpacing } row.Width = width @@ -127,55 +111,28 @@ func (row *Compact) SetWidth(width int) { func (row *Compact) Buffer() ui.Buffer { buf := ui.NewBuffer() - buf.Merge(row.Bg.Buffer()) - buf.Merge(row.Status.Buffer()) - buf.Merge(row.Name.Buffer()) - buf.Merge(row.Cid.Buffer()) - buf.Merge(row.Cpu.Buffer()) - buf.Merge(row.Mem.Buffer()) - buf.Merge(row.Net.Buffer()) - buf.Merge(row.IO.Buffer()) - buf.Merge(row.Pids.Buffer()) + for _, w := range row.Cols { + buf.Merge(w.Buffer()) + } return buf } -func (row *Compact) all() []ui.GridBufferer { - return []ui.GridBufferer{ - row.Status, - row.Name, - row.Cid, - row.Cpu, - row.Mem, - row.Net, - row.IO, - row.Pids, - } -} - func (row *Compact) Highlight() { - row.Name.Highlight() + row.Cols[1].Highlight() if config.GetSwitchVal("fullRowCursor") { - row.Bg.Highlight() - row.Cid.Highlight() - row.Cpu.Highlight() - row.Mem.Highlight() - row.Net.Highlight() - row.IO.Highlight() - row.Pids.Highlight() + for _, w := range row.Cols { + w.Highlight() + } } } func (row *Compact) UnHighlight() { - row.Name.UnHighlight() + row.Cols[1].UnHighlight() if config.GetSwitchVal("fullRowCursor") { - row.Bg.UnHighlight() - row.Cid.UnHighlight() - row.Cpu.UnHighlight() - row.Mem.UnHighlight() - row.Net.UnHighlight() - row.IO.UnHighlight() - row.Pids.UnHighlight() + for _, w := range row.Cols { + w.UnHighlight() + } } } diff --git a/cwidgets/compact/setters.go b/cwidgets/compact/setters.go deleted file mode 100644 index 779991e..0000000 --- a/cwidgets/compact/setters.go +++ /dev/null @@ -1,48 +0,0 @@ -package compact - -import ( - "fmt" - "strconv" - - "github.com/bcicen/ctop/cwidgets" - ui "github.com/gizak/termui" -) - -func (row *Compact) SetNet(rx int64, tx int64) { - label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(rx), cwidgets.ByteFormat(tx)) - 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 := strconv.Itoa(val) - row.Pids.Set(label) -} - -func (row *Compact) SetCPU(val int) { - row.Cpu.BarColor = colorScale(val) - row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val)) - if val < 5 { - val = 5 - row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg") - } - if val > 100 { - val = 100 - } - row.Cpu.Percent = val -} - -func (row *Compact) SetMem(val int64, limit int64, percent int) { - row.Mem.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit)) - if percent < 5 { - percent = 5 - row.Mem.BarColor = ui.ColorBlack - } else { - row.Mem.BarColor = ui.ThemeAttr("gauge.bar.bg") - } - row.Mem.Percent = percent -} diff --git a/cwidgets/compact/status.go b/cwidgets/compact/status.go index d6b920f..43c838b 100644 --- a/cwidgets/compact/status.go +++ b/cwidgets/compact/status.go @@ -1,6 +1,7 @@ package compact import ( + "github.com/bcicen/ctop/models" ui "github.com/gizak/termui" ) @@ -24,7 +25,7 @@ func NewStatus() *Status { } s.Height = 1 s.Border = false - s.Set("") + s.setState("") return s } @@ -43,7 +44,18 @@ func (s *Status) Buffer() ui.Buffer { return buf } -func (s *Status) Set(val string) { +func (s *Status) SetMeta(m models.Meta) { + s.setState(m.Get("state")) + s.setHealth(m.Get("health")) +} + +// Status implements CompactCol +func (s *Status) Reset() {} +func (s *Status) SetMetrics(models.Metrics) {} +func (s *Status) Highlight() {} +func (s *Status) UnHighlight() {} + +func (s *Status) setState(val string) { // defaults text := mark color := ui.ColorDefault @@ -60,21 +72,21 @@ func (s *Status) Set(val string) { s.status = ui.TextCells(text, color, ui.ColorDefault) } -func (s *Status) SetHealth(val string) { - if val == "" { - return - } - +func (s *Status) setHealth(val string) { color := ui.ColorDefault mark := healthMark switch val { + case "": + return case "healthy": color = ui.ThemeAttr("status.ok") case "unhealthy": color = ui.ThemeAttr("status.danger") case "starting": color = ui.ThemeAttr("status.warn") + default: + log.Warningf("unknown health state string: \"%v\"", val) } s.health = ui.TextCells(mark, color, ui.ColorDefault) diff --git a/cwidgets/compact/text.go b/cwidgets/compact/text.go index e635e25..ed690a6 100644 --- a/cwidgets/compact/text.go +++ b/cwidgets/compact/text.go @@ -1,9 +1,56 @@ package compact import ( + "fmt" + + "github.com/bcicen/ctop/cwidgets" + "github.com/bcicen/ctop/models" ui "github.com/gizak/termui" ) +type NameCol struct { + *TextCol +} + +func (w *NameCol) SetMeta(m models.Meta) { + if s, ok := m["name"]; ok { + w.Text = s + } +} + +func (w *NameCol) SetMetrics(m models.Metrics) { +} + +type CIDCol struct { + *TextCol +} + +type NetCol struct { + *TextCol +} + +func (w *NetCol) SetMetrics(m models.Metrics) { + label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(m.NetRx), cwidgets.ByteFormat(m.NetTx)) + w.Text = label +} + +type IOCol struct { + *TextCol +} + +func (w *IOCol) SetMetrics(m models.Metrics) { + label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(m.IOBytesRead), cwidgets.ByteFormat(m.IOBytesWrite)) + w.Text = label +} + +type PIDCol struct { + *TextCol +} + +func (w *PIDCol) SetMetrics(m models.Metrics) { + w.Text = fmt.Sprintf("%d", m.Pids) +} + type TextCol struct { *ui.Par } @@ -28,10 +75,7 @@ func (w *TextCol) UnHighlight() { w.TextBgColor = ui.ThemeAttr("par.text.bg") } -func (w *TextCol) Reset() { - w.Text = "-" -} - -func (w *TextCol) Set(s string) { - w.Text = s -} +//func (w *TextCol) Set(s string) { w.Text = s } +func (w *TextCol) Reset() { w.Text = "-" } +func (w *TextCol) SetMeta(models.Meta) {} +func (w *TextCol) SetMetrics(models.Metrics) {} diff --git a/cwidgets/main.go b/cwidgets/main.go index 50e4d2e..fd2f68b 100644 --- a/cwidgets/main.go +++ b/cwidgets/main.go @@ -8,6 +8,6 @@ import ( var log = logging.Init() type WidgetUpdater interface { - SetMeta(string, string) + SetMeta(models.Meta) SetMetrics(models.Metrics) } diff --git a/cwidgets/single/main.go b/cwidgets/single/main.go index 8140b53..6f7b02c 100644 --- a/cwidgets/single/main.go +++ b/cwidgets/single/main.go @@ -55,11 +55,13 @@ func (e *Single) Down() { } func (e *Single) SetWidth(w int) { e.Width = w } -func (e *Single) SetMeta(k, v string) { - if k == "[ENV-VAR]" { - e.Env.Set(k, v) - } else { - e.Info.Set(k, v) +func (e *Single) SetMeta(m models.Meta) { + for k, v := range m { + if k == "[ENV-VAR]" { + e.Env.Set(k, v) + } else { + e.Info.Set(k, v) + } } } diff --git a/models/main.go b/models/main.go index feb23ce..dfbb30e 100644 --- a/models/main.go +++ b/models/main.go @@ -7,6 +7,17 @@ type Log struct { Message string } +type Meta map[string]string + +func NewMeta() Meta { return make(Meta) } + +func (m Meta) Get(k string) string { + if s, ok := m[k]; ok { + return s + } + return "" +} + type Metrics struct { CPUUtil int NetTx int64 From 7fdcd7bbf1b6ad873d774ece7ee955aec708fc13 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Fri, 5 Jul 2019 23:05:21 +0000 Subject: [PATCH 03/16] continuing compact widget refactor --- container/main.go | 6 +- cwidgets/compact/gauge.go | 20 +++-- cwidgets/compact/grid.go | 54 ++++++++++--- cwidgets/compact/header.go | 65 +++++++--------- cwidgets/compact/main.go | 152 ------------------------------------- cwidgets/compact/status.go | 2 + cwidgets/compact/text.go | 46 ++++++++--- cwidgets/compact/util.go | 13 ---- models/main.go | 18 ++++- 9 files changed, 143 insertions(+), 233 deletions(-) delete mode 100644 cwidgets/compact/main.go diff --git a/container/main.go b/container/main.go index 660e5cc..88b3619 100644 --- a/container/main.go +++ b/container/main.go @@ -22,7 +22,7 @@ type Container struct { models.Metrics Id string Meta models.Meta - Widgets *compact.Compact + Widgets *compact.CompactRow Display bool // display this container in compact view updater cwidgets.WidgetUpdater collector collector.Collector @@ -30,11 +30,11 @@ type Container struct { } func New(id string, collector collector.Collector, manager manager.Manager) *Container { - widgets := compact.NewCompact(id) + widgets := compact.NewCompactRow() return &Container{ Metrics: models.NewMetrics(), Id: id, - Meta: models.NewMeta(), + Meta: models.NewMeta("id", id), Widgets: widgets, updater: widgets, collector: collector, diff --git a/cwidgets/compact/gauge.go b/cwidgets/compact/gauge.go index b22042f..fdf3a03 100644 --- a/cwidgets/compact/gauge.go +++ b/cwidgets/compact/gauge.go @@ -12,6 +12,10 @@ type CPUCol struct { *GaugeCol } +func NewCPUCol() CompactCol { + return &CPUCol{NewGaugeCol("CPU")} +} + func (w *CPUCol) SetMetrics(m models.Metrics) { val := m.CPUUtil w.BarColor = colorScale(val) @@ -27,6 +31,10 @@ type MemCol struct { *GaugeCol } +func NewMemCol() CompactCol { + return &MemCol{NewGaugeCol("MEM")} +} + func (w *MemCol) SetMetrics(m models.Metrics) { w.BarColor = ui.ThemeAttr("gauge.bar.bg") w.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(m.MemUsage), cwidgets.ByteFormat(m.MemLimit)) @@ -35,10 +43,12 @@ func (w *MemCol) SetMetrics(m models.Metrics) { type GaugeCol struct { *ui.Gauge + header string + fWidth int } -func NewGaugeCol() *GaugeCol { - g := &GaugeCol{ui.NewGauge()} +func NewGaugeCol(header string) *GaugeCol { + g := &GaugeCol{ui.NewGauge(), header, 0} g.Height = 1 g.Border = false g.PaddingBottom = 0 @@ -63,10 +73,10 @@ func (w *GaugeCol) Buffer() ui.Buffer { } // GaugeCol implements CompactCol -func (w *GaugeCol) SetMeta(models.Meta) {} - -// GaugeCol implements CompactCol +func (w *GaugeCol) SetMeta(models.Meta) {} func (w *GaugeCol) SetMetrics(models.Metrics) {} +func (w *GaugeCol) Header() string { return w.header } +func (w *GaugeCol) FixedWidth() int { return w.fWidth } // GaugeCol implements CompactCol func (w *GaugeCol) Highlight() { diff --git a/cwidgets/compact/grid.go b/cwidgets/compact/grid.go index 8b483c2..710d7f2 100644 --- a/cwidgets/compact/grid.go +++ b/cwidgets/compact/grid.go @@ -4,11 +4,11 @@ import ( ui "github.com/gizak/termui" ) -var header *CompactHeader - type CompactGrid struct { ui.GridBufferer - Rows []ui.GridBufferer + header *CompactHeader + cols []CompactCol // reference columns + Rows []RowBufferer X, Y int Width int Height int @@ -16,8 +16,13 @@ type CompactGrid struct { } func NewCompactGrid() *CompactGrid { - header = NewCompactHeader() // init column header - return &CompactGrid{} + cg := &CompactGrid{header: NewCompactHeader()} + for _, wFn := range allCols { + w := wFn() + cg.cols = append(cg.cols, w) + cg.header.addFieldPar(w.Header()) + } + return cg } func (cg *CompactGrid) Align() { @@ -28,22 +33,47 @@ func (cg *CompactGrid) Align() { } // update row ypos, width recursively + colWidths := cg.calcWidths() for _, r := range cg.pageRows() { r.SetY(y) y += r.GetHeight() - r.SetWidth(cg.Width) + r.SetWidths(cg.Width, colWidths) } } -func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} } -func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height } +func (cg *CompactGrid) Clear() { cg.Rows = []RowBufferer{} } +func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height } func (cg *CompactGrid) SetX(x int) { cg.X = x } func (cg *CompactGrid) SetY(y int) { cg.Y = y } func (cg *CompactGrid) SetWidth(w int) { cg.Width = w } -func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y } +func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - cg.header.Height - cg.Y } -func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) { - rows = append(rows, header) +// calculate and return per-column width +func (cg *CompactGrid) calcWidths() []int { + var autoCols int + width := cg.Width + colWidths := make([]int, len(cg.cols)) + + for n, w := range cg.cols { + colWidths[n] = w.FixedWidth() + width -= w.FixedWidth() + if w.FixedWidth() == 0 { + autoCols++ + } + } + + spacing := colSpacing * len(cg.cols) + autoWidth := (width - spacing) / autoCols + for n, val := range colWidths { + if val == 0 { + colWidths[n] = autoWidth + } + } + return colWidths +} + +func (cg *CompactGrid) pageRows() (rows []RowBufferer) { + rows = append(rows, cg.header) rows = append(rows, cg.Rows[cg.Offset:]...) return rows } @@ -56,6 +86,6 @@ func (cg *CompactGrid) Buffer() ui.Buffer { return buf } -func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) { +func (cg *CompactGrid) AddRows(rows ...RowBufferer) { cg.Rows = append(cg.Rows, rows...) } diff --git a/cwidgets/compact/header.go b/cwidgets/compact/header.go index 6dbd298..2d5ecc9 100644 --- a/cwidgets/compact/header.go +++ b/cwidgets/compact/header.go @@ -8,63 +8,52 @@ type CompactHeader struct { X, Y int Width int Height int + cols []CompactCol + widths []int pars []*ui.Par } func NewCompactHeader() *CompactHeader { - fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"} - ch := &CompactHeader{} - ch.Height = 2 - for _, f := range fields { - ch.addFieldPar(f) + return &CompactHeader{Height: 2} +} + +func (row *CompactHeader) GetHeight() int { + return row.Height +} + +func (row *CompactHeader) SetWidths(totalWidth int, widths []int) { + x := row.X + + for n, w := range row.pars { + w.SetX(x) + w.SetWidth(widths[n]) + x += widths[n] + colSpacing } - return ch + row.Width = totalWidth } -func (ch *CompactHeader) GetHeight() int { - return ch.Height +func (row *CompactHeader) SetX(x int) { + row.X = x } -func (ch *CompactHeader) SetWidth(w int) { - x := ch.X - autoWidth := calcWidth(w) - for n, col := range ch.pars { - // set column to static width - if colWidths[n] != 0 { - col.SetX(x) - col.SetWidth(colWidths[n]) - x += colWidths[n] - continue - } - col.SetX(x) - col.SetWidth(autoWidth) - x += autoWidth + colSpacing - } - ch.Width = w -} - -func (ch *CompactHeader) SetX(x int) { - ch.X = x -} - -func (ch *CompactHeader) SetY(y int) { - for _, p := range ch.pars { +func (row *CompactHeader) SetY(y int) { + for _, p := range row.pars { p.SetY(y) } - ch.Y = y + row.Y = y } -func (ch *CompactHeader) Buffer() ui.Buffer { +func (row *CompactHeader) Buffer() ui.Buffer { buf := ui.NewBuffer() - for _, p := range ch.pars { + for _, p := range row.pars { buf.Merge(p.Buffer()) } return buf } -func (ch *CompactHeader) addFieldPar(s string) { +func (row *CompactHeader) addFieldPar(s string) { p := ui.NewPar(s) - p.Height = ch.Height + p.Height = row.Height p.Border = false - ch.pars = append(ch.pars, p) + row.pars = append(row.pars, p) } diff --git a/cwidgets/compact/main.go b/cwidgets/compact/main.go deleted file mode 100644 index 9f6cef4..0000000 --- a/cwidgets/compact/main.go +++ /dev/null @@ -1,152 +0,0 @@ -package compact - -import ( - "github.com/bcicen/ctop/config" - "github.com/bcicen/ctop/logging" - "github.com/bcicen/ctop/models" - ui "github.com/gizak/termui" -) - -var log = logging.Init() - -type CompactCol interface { - ui.GridBufferer - Reset() - Highlight() - UnHighlight() - SetMeta(models.Meta) - SetMetrics(models.Metrics) -} - -type Compact struct { - Bg *RowBg - Cols []CompactCol - X, Y int - Width int - Height int -} - -func NewCompact(id string) *Compact { - // truncate container id - if len(id) > 12 { - id = id[:12] - } - row := &Compact{ - Bg: NewRowBg(), - Cols: []CompactCol{ - NewStatus(), - &NameCol{NewTextCol("-")}, - &CIDCol{NewTextCol(id)}, - &CPUCol{NewGaugeCol()}, - &MemCol{NewGaugeCol()}, - &NetCol{NewTextCol("-")}, - &IOCol{NewTextCol("-")}, - &PIDCol{NewTextCol("-")}, - }, - X: 1, - Height: 1, - } - return row -} - -func (row *Compact) SetMeta(m models.Meta) { - for _, w := range row.Cols { - w.SetMeta(m) - } -} - -func (row *Compact) SetMetrics(m models.Metrics) { - for _, w := range row.Cols { - w.SetMetrics(m) - } -} - -// Set gauges, counters, etc. to default unread values -func (row *Compact) Reset() { - for _, w := range row.Cols { - w.Reset() - } -} - -func (row *Compact) GetHeight() int { return row.Height } -func (row *Compact) SetX(x int) { row.X = x } - -func (row *Compact) SetY(y int) { - if y == row.Y { - return - } - - row.Bg.Y = y - for _, w := range row.Cols { - w.SetY(y) - } - row.Y = y -} - -func (row *Compact) SetWidth(width int) { - if width == row.Width { - return - } - x := row.X - - row.Bg.SetX(x + colWidths[0] + 1) - row.Bg.SetWidth(width) - - autoWidth := calcWidth(width) - for n, w := range row.Cols { - // set static width, if provided - if colWidths[n] != 0 { - w.SetX(x) - w.SetWidth(colWidths[n]) - x += colWidths[n] - continue - } - // else use auto width - w.SetX(x) - w.SetWidth(autoWidth) - x += autoWidth + colSpacing - } - row.Width = width -} - -func (row *Compact) Buffer() ui.Buffer { - buf := ui.NewBuffer() - buf.Merge(row.Bg.Buffer()) - for _, w := range row.Cols { - buf.Merge(w.Buffer()) - } - return buf -} - -func (row *Compact) Highlight() { - row.Cols[1].Highlight() - if config.GetSwitchVal("fullRowCursor") { - for _, w := range row.Cols { - w.Highlight() - } - } -} - -func (row *Compact) UnHighlight() { - row.Cols[1].UnHighlight() - if config.GetSwitchVal("fullRowCursor") { - for _, w := range row.Cols { - w.UnHighlight() - } - } -} - -type RowBg struct { - *ui.Par -} - -func NewRowBg() *RowBg { - bg := ui.NewPar("") - bg.Height = 1 - bg.Border = false - bg.Bg = ui.ThemeAttr("par.text.bg") - return &RowBg{bg} -} - -func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") } -func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") } diff --git a/cwidgets/compact/status.go b/cwidgets/compact/status.go index 43c838b..74e30f9 100644 --- a/cwidgets/compact/status.go +++ b/cwidgets/compact/status.go @@ -54,6 +54,8 @@ func (s *Status) Reset() {} func (s *Status) SetMetrics(models.Metrics) {} func (s *Status) Highlight() {} func (s *Status) UnHighlight() {} +func (s *Status) Header() string { return "" } +func (s *Status) FixedWidth() int { return 3 } func (s *Status) setState(val string) { // defaults diff --git a/cwidgets/compact/text.go b/cwidgets/compact/text.go index ed690a6..3ff9440 100644 --- a/cwidgets/compact/text.go +++ b/cwidgets/compact/text.go @@ -12,23 +12,38 @@ type NameCol struct { *TextCol } -func (w *NameCol) SetMeta(m models.Meta) { - if s, ok := m["name"]; ok { - w.Text = s - } +func NewNameCol() CompactCol { + return &NameCol{NewTextCol("NAME")} } -func (w *NameCol) SetMetrics(m models.Metrics) { +func (w *NameCol) SetMeta(m models.Meta) { + w.Text = m.Get("name") + // truncate container id + if len(w.Text) > 12 { + w.Text = w.Text[:12] + } } type CIDCol struct { *TextCol } +func NewCIDCol() CompactCol { + return &CIDCol{NewTextCol("CID")} +} + +func (w *CIDCol) SetMeta(m models.Meta) { + w.Text = m.Get("id") +} + type NetCol struct { *TextCol } +func NewNetCol() CompactCol { + return &NetCol{NewTextCol("NET RX/TX")} +} + func (w *NetCol) SetMetrics(m models.Metrics) { label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(m.NetRx), cwidgets.ByteFormat(m.NetTx)) w.Text = label @@ -38,6 +53,10 @@ type IOCol struct { *TextCol } +func NewIOCol() CompactCol { + return &IOCol{NewTextCol("IO R/W")} +} + func (w *IOCol) SetMetrics(m models.Metrics) { label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(m.IOBytesRead), cwidgets.ByteFormat(m.IOBytesWrite)) w.Text = label @@ -47,20 +66,28 @@ type PIDCol struct { *TextCol } +func NewPIDCol() CompactCol { + w := &PIDCol{NewTextCol("PIDS")} + w.fWidth = 4 + return w +} + func (w *PIDCol) SetMetrics(m models.Metrics) { w.Text = fmt.Sprintf("%d", m.Pids) } type TextCol struct { *ui.Par + header string + fWidth int } -func NewTextCol(s string) *TextCol { - p := ui.NewPar(s) +func NewTextCol(header string) *TextCol { + p := ui.NewPar("-") p.Border = false p.Height = 1 p.Width = 20 - return &TextCol{p} + return &TextCol{p, header, 0} } func (w *TextCol) Highlight() { @@ -75,7 +102,8 @@ func (w *TextCol) UnHighlight() { w.TextBgColor = ui.ThemeAttr("par.text.bg") } -//func (w *TextCol) Set(s string) { w.Text = s } func (w *TextCol) Reset() { w.Text = "-" } func (w *TextCol) SetMeta(models.Meta) {} func (w *TextCol) SetMetrics(models.Metrics) {} +func (w *TextCol) Header() string { return w.header } +func (w *TextCol) FixedWidth() int { return w.fWidth } diff --git a/cwidgets/compact/util.go b/cwidgets/compact/util.go index 9bd8f66..ec9b149 100644 --- a/cwidgets/compact/util.go +++ b/cwidgets/compact/util.go @@ -22,19 +22,6 @@ var colWidths = []int{ 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++ - } - } - return (width - spacing) / staticCols -} - func centerParText(p *ui.Par) { var text string var padding string diff --git a/models/main.go b/models/main.go index dfbb30e..86168b2 100644 --- a/models/main.go +++ b/models/main.go @@ -9,7 +9,23 @@ type Log struct { type Meta map[string]string -func NewMeta() Meta { return make(Meta) } +// NewMeta returns an initialized Meta map. +// An optional series of key, values may be provided to populate the map prior to returning +func NewMeta(kvs ...string) Meta { + m := make(Meta) + + var k string + for i := 0; i < len(kvs)-1; i++ { + if k == "" { + k = kvs[i] + } else { + m[k] = kvs[i] + k = "" + } + } + + return m +} func (m Meta) Get(k string) string { if s, ok := m[k]; ok { From db2c832bd733dc74933afb75e3188cf6c413836a Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Tue, 2 Jul 2019 20:13:46 +0000 Subject: [PATCH 04/16] add run-dev to makefile --- Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 43d0172..730e0b3 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,6 @@ build: go mod download CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop -build-dev: - go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)" - build-all: mkdir -p _build GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64 @@ -23,6 +20,11 @@ build-all: GOOS=windows GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64 cd _build; sha256sum * > sha256sums.txt +run-dev: + rm -f ctop.sock ctop + go build -ldflags $(LD_FLAGS) -o ctop + CTOP_DEBUG=1 ./ctop + image: docker build -t ctop -f Dockerfile . From c8e896e371c6a0464085bee7b378095a2a0b79bd Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Wed, 3 Jul 2019 11:27:17 +0000 Subject: [PATCH 05/16] Revert "shows total memory usage" This reverts commit 1271ce96e8517ea89f128ef41a860cec2438497e. --- cursor.go | 10 ---------- grid.go | 1 - widgets/header.go | 9 +-------- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/cursor.go b/cursor.go index e518531..6c5a6e8 100644 --- a/cursor.go +++ b/cursor.go @@ -17,16 +17,6 @@ type GridCursor struct { func (gc *GridCursor) Len() int { return len(gc.filtered) } -func (gc *GridCursor) MemoryUsage() int64 { - var size int64 - size = 0 - for _, c := range gc.filtered { - size += c.MemUsage - } - gc.Reset() - return size -} - func (gc *GridCursor) Selected() *container.Container { idx := gc.Idx() if idx < gc.Len() { diff --git a/grid.go b/grid.go index d370e1d..655e000 100644 --- a/grid.go +++ b/grid.go @@ -52,7 +52,6 @@ func RedrawRows(clr bool) { y := 1 if config.GetSwitchVal("enableHeader") { header.SetCount(cursor.Len()) - header.SetMemoryUsage(cursor.MemoryUsage()) header.SetFilter(config.GetVal("filterStr")) y += header.Height() } diff --git a/widgets/header.go b/widgets/header.go index e607b84..a7ab786 100644 --- a/widgets/header.go +++ b/widgets/header.go @@ -3,7 +3,7 @@ package widgets import ( "fmt" "time" - "github.com/bcicen/ctop/cwidgets" + ui "github.com/gizak/termui" ) @@ -11,7 +11,6 @@ type CTopHeader struct { Time *ui.Par Count *ui.Par Filter *ui.Par - Mem *ui.Par bg *ui.Par } @@ -20,7 +19,6 @@ func NewCTopHeader() *CTopHeader { Time: headerPar(2, ""), Count: headerPar(24, "-"), Filter: headerPar(40, ""), - Mem: headerPar(70, ""), bg: headerBg(), } } @@ -32,7 +30,6 @@ func (c *CTopHeader) Buffer() ui.Buffer { buf.Merge(c.Time.Buffer()) buf.Merge(c.Count.Buffer()) buf.Merge(c.Filter.Buffer()) - buf.Merge(c.Mem.Buffer()) return buf } @@ -61,10 +58,6 @@ func headerBg() *ui.Par { return bg } -func (c *CTopHeader) SetMemoryUsage(val int64) { - c.Mem.Text = cwidgets.ByteFormat(val) -} - func (c *CTopHeader) SetCount(val int) { c.Count.Text = fmt.Sprintf("%d containers", val) } From d56cc9475afcfa47cbd8ecf3f296b8ece5807e50 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Wed, 6 Nov 2019 12:31:57 +0000 Subject: [PATCH 06/16] update sig --- cwidgets/compact/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwidgets/compact/status.go b/cwidgets/compact/status.go index 74e30f9..fae7bdc 100644 --- a/cwidgets/compact/status.go +++ b/cwidgets/compact/status.go @@ -18,7 +18,7 @@ type Status struct { health []ui.Cell } -func NewStatus() *Status { +func NewStatus() CompactCol { s := &Status{ Block: ui.NewBlock(), health: []ui.Cell{{Ch: ' '}}, From 44601623803156b50c0b3b9d16d91b4fe3cd81d7 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Wed, 6 Nov 2019 12:32:03 +0000 Subject: [PATCH 07/16] go 1.13 --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index cb8db3f..53ade78 100644 --- a/go.mod +++ b/go.mod @@ -28,3 +28,5 @@ require ( ) replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 + +go 1.13 From d34b9c2bf649306ee0332c6e1cf64f8ced91e060 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Mon, 11 Nov 2019 22:04:25 +0000 Subject: [PATCH 08/16] remove unused static col width --- cwidgets/compact/util.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cwidgets/compact/util.go b/cwidgets/compact/util.go index ec9b149..9722c54 100644 --- a/cwidgets/compact/util.go +++ b/cwidgets/compact/util.go @@ -10,18 +10,6 @@ import ( const colSpacing = 1 -// per-column width. 0 == auto width -var colWidths = []int{ - 5, // status - 0, // name - 0, // cid - 0, // cpu - 0, // memory - 0, // net - 0, // io - 4, // pids -} - func centerParText(p *ui.Par) { var text string var padding string From ee25f80a9c1ca781164dad1cb040eb3c6718ba93 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Thu, 2 Jan 2020 14:00:55 +0000 Subject: [PATCH 09/16] integrate widget order, toggling into global config and compact grid --- config/file.go | 15 +++++ config/main.go | 15 +++-- config/param.go | 10 ++- config/switch.go | 17 +++++- config/widget.go | 122 +++++++++++++++++++++++++++++++++++++ cwidgets/compact/gauge.go | 1 + cwidgets/compact/grid.go | 14 +++-- cwidgets/compact/header.go | 4 ++ cwidgets/compact/status.go | 1 + cwidgets/compact/text.go | 1 + 10 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 config/widget.go diff --git a/config/file.go b/config/file.go index 92bec57..4019807 100644 --- a/config/file.go +++ b/config/file.go @@ -16,22 +16,33 @@ var ( type File struct { Options map[string]string `toml:"options"` Toggles map[string]bool `toml:"toggles"` + Widgets []Widget `toml:"widget"` } func exportConfig() File { + lock.RLock() + defer lock.RUnlock() + c := File{ Options: make(map[string]string), Toggles: make(map[string]bool), + Widgets: make([]Widget, len(GlobalWidgets)), } + for _, p := range GlobalParams { c.Options[p.Key] = p.Val } for _, sw := range GlobalSwitches { c.Toggles[sw.Key] = sw.Val } + for n, w := range GlobalWidgets { + c.Widgets[n] = *w + } + return c } +// func Read() error { var config File @@ -50,6 +61,10 @@ func Read() error { for k, v := range config.Toggles { UpdateSwitch(k, v) } + for _, w := range config.Widgets { + UpdateWidget(strings.ToLower(w.Name), w.Enabled) + } + return nil } diff --git a/config/main.go b/config/main.go index 2b4b997..cb873cc 100644 --- a/config/main.go +++ b/config/main.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "sync" "github.com/bcicen/ctop/logging" ) @@ -10,17 +11,23 @@ import ( var ( GlobalParams []*Param GlobalSwitches []*Switch + GlobalWidgets []*Widget + lock sync.RWMutex log = logging.Init() ) func Init() { - for _, p := range params { + for _, p := range defaultParams { GlobalParams = append(GlobalParams, p) - log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val)) + log.Infof("loaded default config param: %s: %s", quote(p.Key), quote(p.Val)) } - for _, s := range switches { + for _, s := range defaultSwitches { GlobalSwitches = append(GlobalSwitches, s) - log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val) + log.Infof("loaded default config switch: %s: %t", quote(s.Key), s.Val) + } + for _, w := range defaultWidgets { + GlobalWidgets = append(GlobalWidgets, w) + log.Infof("loaded default widget: %s: %t", quote(w.Name), w.Enabled) } } diff --git a/config/param.go b/config/param.go index 86f5828..6233d5d 100644 --- a/config/param.go +++ b/config/param.go @@ -1,7 +1,7 @@ package config // defaults -var params = []*Param{ +var defaultParams = []*Param{ &Param{ Key: "filterStr", Val: "", @@ -27,6 +27,9 @@ type Param struct { // Get Param by key func Get(k string) *Param { + lock.RLock() + defer lock.RUnlock() + for _, p := range GlobalParams { if p.Key == k { return p @@ -43,7 +46,10 @@ func GetVal(k string) string { // Set param value func Update(k, v string) { p := Get(k) - log.Noticef("config change: %s: %s -> %s", k, quote(p.Val), quote(v)) + log.Noticef("config change [%s]: %s -> %s", k, quote(p.Val), quote(v)) + + lock.Lock() + defer lock.Unlock() p.Val = v // log.Errorf("ignoring update for non-existant parameter: %s", k) } diff --git a/config/switch.go b/config/switch.go index ab3d7e0..b5c3673 100644 --- a/config/switch.go +++ b/config/switch.go @@ -1,7 +1,7 @@ package config // defaults -var switches = []*Switch{ +var defaultSwitches = []*Switch{ &Switch{ Key: "sortReversed", Val: false, @@ -37,6 +37,9 @@ type Switch struct { // GetSwitch returns Switch by key func GetSwitch(k string) *Switch { + lock.RLock() + defer lock.RUnlock() + for _, sw := range GlobalSwitches { if sw.Key == k { return sw @@ -52,8 +55,12 @@ func GetSwitchVal(k string) bool { func UpdateSwitch(k string, val bool) { sw := GetSwitch(k) + + lock.Lock() + defer lock.Unlock() + if sw.Val != val { - log.Noticef("config change: %s: %t -> %t", k, sw.Val, val) + log.Noticef("config change [%s]: %t -> %t", k, sw.Val, val) sw.Val = val } } @@ -61,8 +68,12 @@ func UpdateSwitch(k string, val bool) { // Toggle a boolean switch func Toggle(k string) { sw := GetSwitch(k) + + lock.Lock() + defer lock.Unlock() + newVal := !sw.Val - log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal) + log.Noticef("config change [%s]: %t -> %t", k, sw.Val, newVal) sw.Val = newVal //log.Errorf("ignoring toggle for non-existant switch: %s", k) } diff --git a/config/widget.go b/config/widget.go new file mode 100644 index 0000000..3783950 --- /dev/null +++ b/config/widget.go @@ -0,0 +1,122 @@ +package config + +import ( + "strings" +) + +// defaults +var defaultWidgets = []*Widget{ + &Widget{ + Name: "status", + Enabled: true, + }, + &Widget{ + Name: "name", + Enabled: true, + }, + &Widget{ + Name: "id", + Enabled: true, + }, + &Widget{ + Name: "cpu", + Enabled: true, + }, + &Widget{ + Name: "mem", + Enabled: true, + }, + &Widget{ + Name: "net", + Enabled: true, + }, + &Widget{ + Name: "io", + Enabled: true, + }, + &Widget{ + Name: "pids", + Enabled: true, + }, +} + +type Widget struct { + Name string + Enabled bool +} + +// GetWidget returns a Widget by name +func GetWidget(name string) *Widget { + lock.RLock() + defer lock.RUnlock() + + for _, w := range GlobalWidgets { + if w.Name == name { + return w + } + } + log.Errorf("widget name not found: %s", name) + return &Widget{} // default +} + +// Widgets returns a copy of all configurable Widgets, in order +func Widgets() []Widget { + a := make([]Widget, len(GlobalWidgets)) + + lock.RLock() + defer lock.RUnlock() + + for n, w := range GlobalWidgets { + a[n] = *w + } + return a +} + +// EnabledWidgets returns an ordered array of enabled widget names +func EnabledWidgets() (a []string) { + for _, w := range Widgets() { + if w.Enabled { + a = append(a, w.Name) + } + } + return a +} + +func UpdateWidget(name string, enabled bool) { + w := GetWidget(name) + oldVal := w.Enabled + log.Noticef("config change [%s-enabled]: %t -> %t", name, oldVal, enabled) + + lock.Lock() + defer lock.Unlock() + w.Enabled = enabled +} + +func ToggleWidgetEnabled(name string) { + w := GetWidget(name) + newVal := !w.Enabled + log.Noticef("config change [%s-enabled]: %t -> %t", name, w.Enabled, newVal) + + lock.Lock() + defer lock.Unlock() + w.Enabled = newVal +} + +// UpdateWidgets replaces existing ordered widgets with those provided +func UpdateWidgets(newWidgets []Widget) { + oldOrder := widgetNames() + lock.Lock() + for n, w := range newWidgets { + GlobalWidgets[n] = &w + } + lock.Unlock() + log.Noticef("config change [widget-order]: %s -> %s", oldOrder, widgetNames()) +} + +func widgetNames() string { + a := make([]string, len(GlobalWidgets)) + for n, w := range Widgets() { + a[n] = w.Name + } + return strings.Join(a, ", ") +} diff --git a/cwidgets/compact/gauge.go b/cwidgets/compact/gauge.go index fdf3a03..0862569 100644 --- a/cwidgets/compact/gauge.go +++ b/cwidgets/compact/gauge.go @@ -5,6 +5,7 @@ import ( "github.com/bcicen/ctop/cwidgets" "github.com/bcicen/ctop/models" + ui "github.com/gizak/termui" ) diff --git a/cwidgets/compact/grid.go b/cwidgets/compact/grid.go index 710d7f2..3e95ec7 100644 --- a/cwidgets/compact/grid.go +++ b/cwidgets/compact/grid.go @@ -17,11 +17,7 @@ type CompactGrid struct { func NewCompactGrid() *CompactGrid { cg := &CompactGrid{header: NewCompactHeader()} - for _, wFn := range allCols { - w := wFn() - cg.cols = append(cg.cols, w) - cg.header.addFieldPar(w.Header()) - } + cg.RebuildHeader() return cg } @@ -41,6 +37,14 @@ func (cg *CompactGrid) Align() { } } +func (cg *CompactGrid) RebuildHeader() { + cg.cols = newRowWidgets() + cg.header.clearFieldPars() + for _, col := range cg.cols { + cg.header.addFieldPar(col.Header()) + } +} + func (cg *CompactGrid) Clear() { cg.Rows = []RowBufferer{} } func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height } func (cg *CompactGrid) SetX(x int) { cg.X = x } diff --git a/cwidgets/compact/header.go b/cwidgets/compact/header.go index 2d5ecc9..bec7533 100644 --- a/cwidgets/compact/header.go +++ b/cwidgets/compact/header.go @@ -51,6 +51,10 @@ func (row *CompactHeader) Buffer() ui.Buffer { return buf } +func (row *CompactHeader) clearFieldPars() { + row.pars = []*ui.Par{} +} + func (row *CompactHeader) addFieldPar(s string) { p := ui.NewPar(s) p.Height = row.Height diff --git a/cwidgets/compact/status.go b/cwidgets/compact/status.go index fae7bdc..b5bb261 100644 --- a/cwidgets/compact/status.go +++ b/cwidgets/compact/status.go @@ -2,6 +2,7 @@ package compact import ( "github.com/bcicen/ctop/models" + ui "github.com/gizak/termui" ) diff --git a/cwidgets/compact/text.go b/cwidgets/compact/text.go index 3ff9440..dfc9f53 100644 --- a/cwidgets/compact/text.go +++ b/cwidgets/compact/text.go @@ -5,6 +5,7 @@ import ( "github.com/bcicen/ctop/cwidgets" "github.com/bcicen/ctop/models" + ui "github.com/gizak/termui" ) From 118b89240d72518a8a9b0abffd8b69e37d26351d Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Thu, 2 Jan 2020 19:28:51 +0000 Subject: [PATCH 10/16] refactor column config --- config/columns.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++ config/file.go | 20 ++++--- config/main.go | 13 +++-- config/param.go | 5 ++ config/widget.go | 122 ---------------------------------------- 5 files changed, 162 insertions(+), 136 deletions(-) create mode 100644 config/columns.go delete mode 100644 config/widget.go diff --git a/config/columns.go b/config/columns.go new file mode 100644 index 0000000..90d7b38 --- /dev/null +++ b/config/columns.go @@ -0,0 +1,138 @@ +package config + +import ( + "strings" +) + +// defaults +var defaultColumns = []Column{ + Column{ + Name: "status", + Label: "Status Indicator", + Enabled: true, + }, + Column{ + Name: "name", + Label: "Container Name", + Enabled: true, + }, + Column{ + Name: "id", + Label: "Container ID", + Enabled: true, + }, + Column{ + Name: "cpu", + Label: "CPU Usage", + Enabled: true, + }, + Column{ + Name: "mem", + Label: "Memory Usage", + Enabled: true, + }, + Column{ + Name: "net", + Label: "Network RX/TX", + Enabled: true, + }, + Column{ + Name: "io", + Label: "Disk IO Read/Write", + Enabled: true, + }, + Column{ + Name: "pids", + Label: "Container PID Count", + Enabled: true, + }, +} + +type Column struct { + Name string + Label string + Enabled bool +} + +// ColumnsString returns an ordered and comma-delimited string of currently enabled Columns +func ColumnsString() string { return strings.Join(EnabledColumns(), ",") } + +// EnabledColumns returns an ordered array of enabled column names +func EnabledColumns() (a []string) { + lock.RLock() + defer lock.RUnlock() + for _, col := range GlobalColumns { + if col.Enabled { + a = append(a, col.Name) + } + } + return a +} + +// ColumnLeft moves the column with given name up one position, if possible +func ColumnLeft(name string) { + idx := colIndex(name) + if idx > 0 { + swapCols(idx, idx-1) + } +} + +// ColumnRight moves the column with given name up one position, if possible +func ColumnRight(name string) { + idx := colIndex(name) + if idx < len(GlobalColumns)-1 { + swapCols(idx, idx+1) + } +} + +// Set Column order and enabled status from one or more provided Column names +func SetColumns(names []string) { + var ( + n int + curColStr = ColumnsString() + newColumns = make([]*Column, len(GlobalColumns)) + ) + + lock.Lock() + + // add enabled columns by name + for _, name := range names { + newColumns[n] = popColumn(name) + newColumns[n].Enabled = true + n++ + } + + // extend with omitted columns as disabled + for _, col := range GlobalColumns { + newColumns[n] = col + newColumns[n].Enabled = false + n++ + } + + GlobalColumns = newColumns + lock.Unlock() + + log.Noticef("config change [columns]: %s -> %s", curColStr, ColumnsString()) +} + +func swapCols(i, j int) { GlobalColumns[i], GlobalColumns[j] = GlobalColumns[j], GlobalColumns[i] } + +func popColumn(name string) *Column { + idx := colIndex(name) + if idx < 0 { + panic("no such column name: " + name) + } + col := GlobalColumns[idx] + GlobalColumns = append(GlobalColumns[:idx], GlobalColumns[idx+1:]...) + return col +} + +// return index of column with given name, if any +func colIndex(name string) int { + for n, c := range GlobalColumns { + if c.Name == name { + return n + } + } + return -1 +} diff --git a/config/file.go b/config/file.go index 4019807..fc08a06 100644 --- a/config/file.go +++ b/config/file.go @@ -16,17 +16,18 @@ var ( type File struct { Options map[string]string `toml:"options"` Toggles map[string]bool `toml:"toggles"` - Widgets []Widget `toml:"widget"` } func exportConfig() File { + // update columns param from working config + Update("columns", ColumnsString()) + lock.RLock() defer lock.RUnlock() c := File{ Options: make(map[string]string), Toggles: make(map[string]bool), - Widgets: make([]Widget, len(GlobalWidgets)), } for _, p := range GlobalParams { @@ -35,9 +36,6 @@ func exportConfig() File { for _, sw := range GlobalSwitches { c.Toggles[sw.Key] = sw.Val } - for n, w := range GlobalWidgets { - c.Widgets[n] = *w - } return c } @@ -54,15 +52,21 @@ func Read() error { if _, err := toml.DecodeFile(path, &config); err != nil { return err } - for k, v := range config.Options { Update(k, v) } for k, v := range config.Toggles { UpdateSwitch(k, v) } - for _, w := range config.Widgets { - UpdateWidget(strings.ToLower(w.Name), w.Enabled) + + // set working column config, if provided + colStr := GetVal("columns") + if len(colStr) > 0 { + var colNames []string + for _, s := range strings.Split(colStr, ",") { + colNames = append(colNames, strings.TrimSpace(s)) + } + SetColumns(colNames) } return nil diff --git a/config/main.go b/config/main.go index cb873cc..d856ab5 100644 --- a/config/main.go +++ b/config/main.go @@ -11,7 +11,7 @@ import ( var ( GlobalParams []*Param GlobalSwitches []*Switch - GlobalWidgets []*Widget + GlobalColumns []*Column lock sync.RWMutex log = logging.Init() ) @@ -19,15 +19,16 @@ var ( func Init() { for _, p := range defaultParams { GlobalParams = append(GlobalParams, p) - log.Infof("loaded default config param: %s: %s", quote(p.Key), quote(p.Val)) + log.Infof("loaded default config param [%s]: %s", quote(p.Key), quote(p.Val)) } for _, s := range defaultSwitches { GlobalSwitches = append(GlobalSwitches, s) - log.Infof("loaded default config switch: %s: %t", quote(s.Key), s.Val) + log.Infof("loaded default config switch [%s]: %t", quote(s.Key), s.Val) } - for _, w := range defaultWidgets { - GlobalWidgets = append(GlobalWidgets, w) - log.Infof("loaded default widget: %s: %t", quote(w.Name), w.Enabled) + for _, c := range defaultColumns { + x := c + GlobalColumns = append(GlobalColumns, &x) + log.Infof("loaded default widget config [%s]: %t", quote(x.Name), x.Enabled) } } diff --git a/config/param.go b/config/param.go index 6233d5d..dc334f5 100644 --- a/config/param.go +++ b/config/param.go @@ -17,6 +17,11 @@ var defaultParams = []*Param{ Val: "sh", Label: "Shell", }, + &Param{ + Key: "columns", + Val: "status,name,id,cpu,mem,net,io,pids", + Label: "Enabled Columns", + }, } type Param struct { diff --git a/config/widget.go b/config/widget.go deleted file mode 100644 index 3783950..0000000 --- a/config/widget.go +++ /dev/null @@ -1,122 +0,0 @@ -package config - -import ( - "strings" -) - -// defaults -var defaultWidgets = []*Widget{ - &Widget{ - Name: "status", - Enabled: true, - }, - &Widget{ - Name: "name", - Enabled: true, - }, - &Widget{ - Name: "id", - Enabled: true, - }, - &Widget{ - Name: "cpu", - Enabled: true, - }, - &Widget{ - Name: "mem", - Enabled: true, - }, - &Widget{ - Name: "net", - Enabled: true, - }, - &Widget{ - Name: "io", - Enabled: true, - }, - &Widget{ - Name: "pids", - Enabled: true, - }, -} - -type Widget struct { - Name string - Enabled bool -} - -// GetWidget returns a Widget by name -func GetWidget(name string) *Widget { - lock.RLock() - defer lock.RUnlock() - - for _, w := range GlobalWidgets { - if w.Name == name { - return w - } - } - log.Errorf("widget name not found: %s", name) - return &Widget{} // default -} - -// Widgets returns a copy of all configurable Widgets, in order -func Widgets() []Widget { - a := make([]Widget, len(GlobalWidgets)) - - lock.RLock() - defer lock.RUnlock() - - for n, w := range GlobalWidgets { - a[n] = *w - } - return a -} - -// EnabledWidgets returns an ordered array of enabled widget names -func EnabledWidgets() (a []string) { - for _, w := range Widgets() { - if w.Enabled { - a = append(a, w.Name) - } - } - return a -} - -func UpdateWidget(name string, enabled bool) { - w := GetWidget(name) - oldVal := w.Enabled - log.Noticef("config change [%s-enabled]: %t -> %t", name, oldVal, enabled) - - lock.Lock() - defer lock.Unlock() - w.Enabled = enabled -} - -func ToggleWidgetEnabled(name string) { - w := GetWidget(name) - newVal := !w.Enabled - log.Noticef("config change [%s-enabled]: %t -> %t", name, w.Enabled, newVal) - - lock.Lock() - defer lock.Unlock() - w.Enabled = newVal -} - -// UpdateWidgets replaces existing ordered widgets with those provided -func UpdateWidgets(newWidgets []Widget) { - oldOrder := widgetNames() - lock.Lock() - for n, w := range newWidgets { - GlobalWidgets[n] = &w - } - lock.Unlock() - log.Noticef("config change [widget-order]: %s -> %s", oldOrder, widgetNames()) -} - -func widgetNames() string { - a := make([]string, len(GlobalWidgets)) - for n, w := range Widgets() { - a[n] = w.Name - } - return strings.Join(a, ", ") -} From 6e60fc905e280cbaeb4cf3fd78cb1a926df85b01 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Thu, 2 Jan 2020 19:29:20 +0000 Subject: [PATCH 11/16] handle empty entries in column config --- config/file.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/file.go b/config/file.go index fc08a06..2fff0ab 100644 --- a/config/file.go +++ b/config/file.go @@ -64,7 +64,10 @@ func Read() error { if len(colStr) > 0 { var colNames []string for _, s := range strings.Split(colStr, ",") { - colNames = append(colNames, strings.TrimSpace(s)) + s = strings.TrimSpace(s) + if s != "" { + colNames = append(colNames, strings.TrimSpace(s)) + } } SetColumns(colNames) } From 22a560701276281d2ba7f5446af68b0756fc1650 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Thu, 2 Jan 2020 23:02:53 +0000 Subject: [PATCH 12/16] init column config menu --- config/columns.go | 7 ++++ config/switch.go | 5 ++- container/main.go | 6 ++++ cwidgets/compact/grid.go | 21 +++++++----- cwidgets/compact/header.go | 5 ++- cwidgets/main.go | 8 +++++ grid.go | 4 +++ menus.go | 67 ++++++++++++++++++++++++++++++++++++-- widgets/menu/main.go | 9 +++++ 9 files changed, 117 insertions(+), 15 deletions(-) diff --git a/config/columns.go b/config/columns.go index 90d7b38..b59a50e 100644 --- a/config/columns.go +++ b/config/columns.go @@ -69,6 +69,13 @@ func EnabledColumns() (a []string) { return a } +// ColumnToggle toggles the enabled status of a given column name +func ColumnToggle(name string) { + col := GlobalColumns[colIndex(name)] + col.Enabled = !col.Enabled + log.Noticef("config change [column-%s]: %t -> %t", col.Name, !col.Enabled, col.Enabled) +} + // ColumnLeft moves the column with given name up one position, if possible func ColumnLeft(name string) { idx := colIndex(name) diff --git a/config/switch.go b/config/switch.go index b5c3673..dca9bc4 100644 --- a/config/switch.go +++ b/config/switch.go @@ -72,8 +72,7 @@ func Toggle(k string) { lock.Lock() defer lock.Unlock() - newVal := !sw.Val - log.Noticef("config change [%s]: %t -> %t", k, sw.Val, newVal) - sw.Val = newVal + sw.Val = !sw.Val + log.Noticef("config change [%s]: %t -> %t", k, !sw.Val, sw.Val) //log.Errorf("ignoring toggle for non-existant switch: %s", k) } diff --git a/container/main.go b/container/main.go index 88b3619..e0b0c3f 100644 --- a/container/main.go +++ b/container/main.go @@ -42,6 +42,12 @@ func New(id string, collector collector.Collector, manager manager.Manager) *Con } } +func (c *Container) RecreateWidgets() { + c.SetUpdater(cwidgets.NullWidgetUpdater{}) + c.Widgets = compact.NewCompactRow() + c.SetUpdater(c.Widgets) +} + func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) { c.updater = u c.updater.SetMeta(c.Meta) diff --git a/cwidgets/compact/grid.go b/cwidgets/compact/grid.go index 3e95ec7..5a3f99f 100644 --- a/cwidgets/compact/grid.go +++ b/cwidgets/compact/grid.go @@ -17,7 +17,7 @@ type CompactGrid struct { func NewCompactGrid() *CompactGrid { cg := &CompactGrid{header: NewCompactHeader()} - cg.RebuildHeader() + cg.rebuildHeader() return cg } @@ -30,6 +30,7 @@ func (cg *CompactGrid) Align() { // update row ypos, width recursively colWidths := cg.calcWidths() + cg.header.SetWidths(cg.Width, colWidths) for _, r := range cg.pageRows() { r.SetY(y) y += r.GetHeight() @@ -37,15 +38,11 @@ func (cg *CompactGrid) Align() { } } -func (cg *CompactGrid) RebuildHeader() { - cg.cols = newRowWidgets() - cg.header.clearFieldPars() - for _, col := range cg.cols { - cg.header.addFieldPar(col.Header()) - } +func (cg *CompactGrid) Clear() { + cg.Rows = []RowBufferer{} + cg.rebuildHeader() } -func (cg *CompactGrid) Clear() { cg.Rows = []RowBufferer{} } func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height } func (cg *CompactGrid) SetX(x int) { cg.X = x } func (cg *CompactGrid) SetY(y int) { cg.Y = y } @@ -93,3 +90,11 @@ func (cg *CompactGrid) Buffer() ui.Buffer { func (cg *CompactGrid) AddRows(rows ...RowBufferer) { cg.Rows = append(cg.Rows, rows...) } + +func (cg *CompactGrid) rebuildHeader() { + cg.cols = newRowWidgets() + cg.header.clearFieldPars() + for _, col := range cg.cols { + cg.header.addFieldPar(col.Header()) + } +} diff --git a/cwidgets/compact/header.go b/cwidgets/compact/header.go index bec7533..44fe5af 100644 --- a/cwidgets/compact/header.go +++ b/cwidgets/compact/header.go @@ -14,7 +14,10 @@ type CompactHeader struct { } func NewCompactHeader() *CompactHeader { - return &CompactHeader{Height: 2} + return &CompactHeader{ + X: rowPadding, + Height: 2, + } } func (row *CompactHeader) GetHeight() int { diff --git a/cwidgets/main.go b/cwidgets/main.go index fd2f68b..2bb8e89 100644 --- a/cwidgets/main.go +++ b/cwidgets/main.go @@ -11,3 +11,11 @@ type WidgetUpdater interface { SetMeta(models.Meta) SetMetrics(models.Metrics) } + +type NullWidgetUpdater struct{} + +// NullWidgetUpdater implements WidgetUpdater +func (wu NullWidgetUpdater) SetMeta(models.Meta) {} + +// NullWidgetUpdater implements WidgetUpdater +func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {} diff --git a/grid.go b/grid.go index 655e000..9efced3 100644 --- a/grid.go +++ b/grid.go @@ -191,6 +191,10 @@ func Display() bool { menu = SortMenu ui.StopLoop() }) + ui.Handle("/sys/kbd/c", func(ui.Event) { + menu = ColumnsMenu + ui.StopLoop() + }) ui.Handle("/sys/kbd/S", func(ui.Event) { path, err := config.Write() if err == nil { diff --git a/menus.go b/menus.go index 16c18b7..55aa88b 100644 --- a/menus.go +++ b/menus.go @@ -104,7 +104,68 @@ func SortMenu() MenuFn { HandleKeys("exit", ui.StopLoop) ui.Handle("/sys/kbd/", func(ui.Event) { - config.Update("sortField", m.SelectedItem().Val) + config.Update("sortField", m.SelectedValue()) + ui.StopLoop() + }) + + ui.Render(m) + ui.Loop() + return nil +} + +func ColumnsMenu() MenuFn { + ui.Clear() + ui.DefaultEvtStream.ResetHandlers() + defer ui.DefaultEvtStream.ResetHandlers() + + m := menu.NewMenu() + m.Selectable = true + m.SortItems = false + m.BorderLabel = "Columns" + + rebuild := func() { + m.ClearItems() + for _, col := range config.GlobalColumns { + txt := fmt.Sprintf("%s [%t]", col.Label, col.Enabled) + m.AddItems(menu.Item{col.Name, txt}) + } + } + + upFn := func() { + config.ColumnLeft(m.SelectedValue()) + m.Up() + rebuild() + } + + downFn := func() { + config.ColumnRight(m.SelectedValue()) + m.Down() + rebuild() + } + + toggleFn := func() { + config.ColumnToggle(m.SelectedValue()) + rebuild() + } + + rebuild() + + HandleKeys("up", m.Up) + HandleKeys("down", m.Down) + HandleKeys("enter", toggleFn) + HandleKeys("pgup", upFn) + HandleKeys("pgdown", downFn) + + ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() }) + ui.Handle("/sys/kbd/", func(ui.Event) { toggleFn() }) + + HandleKeys("exit", func() { + cSource, err := cursor.cSuper.Get() + if err == nil { + for _, c := range cSource.All() { + c.RecreateWidgets() + } + } ui.StopLoop() }) @@ -202,7 +263,7 @@ func ContainerMenu() MenuFn { }) ui.Handle("/sys/kbd/", func(ui.Event) { - selected = m.SelectedItem().Val + selected = m.SelectedValue() ui.StopLoop() }) ui.Handle("/sys/kbd/", func(ui.Event) { @@ -321,7 +382,7 @@ func Confirm(txt string, fn func()) MenuFn { ui.Handle("/sys/kbd/y", func(ui.Event) { yes() }) ui.Handle("/sys/kbd/", func(ui.Event) { - switch m.SelectedItem().Val { + switch m.SelectedValue() { case "cancel": no() case "yes": diff --git a/widgets/menu/main.go b/widgets/menu/main.go index 4e4e83b..e6f2df1 100644 --- a/widgets/menu/main.go +++ b/widgets/menu/main.go @@ -55,6 +55,11 @@ func (m *Menu) DelItem(s string) (success bool) { return success } +// ClearItems removes all current menu items +func (m *Menu) ClearItems() { + m.items = m.items[:0] +} + // Move cursor to an position by Item value or label func (m *Menu) SetCursor(s string) (success bool) { for n, i := range m.items { @@ -79,6 +84,10 @@ func (m *Menu) SelectedItem() Item { return m.items[m.cursorPos] } +func (m *Menu) SelectedValue() string { + return m.items[m.cursorPos].Val +} + func (m *Menu) Buffer() ui.Buffer { var cell ui.Cell buf := m.Block.Buffer() From 50d1c29d579c8cbbdfce51d80d43d65ea5d923aa Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Fri, 3 Jan 2020 12:07:21 +0000 Subject: [PATCH 13/16] handle single kv pair given to NewMeta --- models/main.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/models/main.go b/models/main.go index 86168b2..aa6e539 100644 --- a/models/main.go +++ b/models/main.go @@ -14,14 +14,10 @@ type Meta map[string]string func NewMeta(kvs ...string) Meta { m := make(Meta) - var k string - for i := 0; i < len(kvs)-1; i++ { - if k == "" { - k = kvs[i] - } else { - m[k] = kvs[i] - k = "" - } + var i int + for i < len(kvs)-1 { + m[kvs[i]] = kvs[i+1] + i += 2 } return m From ca5d40b7ccd8fcc10028dbc263f412a5040d370b Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Fri, 3 Jan 2020 12:07:54 +0000 Subject: [PATCH 14/16] truncate id in compact widget --- cwidgets/compact/text.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cwidgets/compact/text.go b/cwidgets/compact/text.go index dfc9f53..4b419a0 100644 --- a/cwidgets/compact/text.go +++ b/cwidgets/compact/text.go @@ -35,6 +35,9 @@ func NewCIDCol() CompactCol { func (w *CIDCol) SetMeta(m models.Meta) { w.Text = m.Get("id") + if len(w.Text) > 12 { + w.Text = w.Text[:12] + } } type NetCol struct { From 5585a22962c48289749f7e2000a18581ad6335a0 Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Fri, 3 Jan 2020 12:25:54 +0000 Subject: [PATCH 15/16] add padding to column menu --- cwidgets/compact/grid.go | 1 - menus.go | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cwidgets/compact/grid.go b/cwidgets/compact/grid.go index 5a3f99f..aa1f376 100644 --- a/cwidgets/compact/grid.go +++ b/cwidgets/compact/grid.go @@ -30,7 +30,6 @@ func (cg *CompactGrid) Align() { // update row ypos, width recursively colWidths := cg.calcWidths() - cg.header.SetWidths(cg.Width, colWidths) for _, r := range cg.pageRows() { r.SetY(y) y += r.GetHeight() diff --git a/menus.go b/menus.go index 55aa88b..f891cd8 100644 --- a/menus.go +++ b/menus.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "time" "github.com/bcicen/ctop/config" @@ -114,6 +115,12 @@ func SortMenu() MenuFn { } func ColumnsMenu() MenuFn { + const ( + enabledStr = "[X]" + disabledStr = "[ ]" + padding = 2 + ) + ui.Clear() ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers() @@ -124,9 +131,24 @@ func ColumnsMenu() MenuFn { m.BorderLabel = "Columns" rebuild := func() { + // get padding for right alignment of enabled status + var maxLen int + for _, col := range config.GlobalColumns { + if len(col.Label) > maxLen { + maxLen = len(col.Label) + } + } + maxLen += padding + + // rebuild menu items m.ClearItems() for _, col := range config.GlobalColumns { - txt := fmt.Sprintf("%s [%t]", col.Label, col.Enabled) + txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label)) + if col.Enabled { + txt += enabledStr + } else { + txt += disabledStr + } m.AddItems(menu.Item{col.Name, txt}) } } From bf3b89a010ac09f102fb2e0f48f7f143d17de99c Mon Sep 17 00:00:00 2001 From: Bradley Cicenas Date: Fri, 3 Jan 2020 12:53:25 +0000 Subject: [PATCH 16/16] add optional tooltip to menu widget --- menus.go | 1 + widgets/menu/main.go | 26 +++++++++++++------ widgets/menu/tooltip.go | 55 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 widgets/menu/tooltip.go diff --git a/menus.go b/menus.go index f891cd8..1b15281 100644 --- a/menus.go +++ b/menus.go @@ -129,6 +129,7 @@ func ColumnsMenu() MenuFn { m.Selectable = true m.SortItems = false m.BorderLabel = "Columns" + //m.SubText = "Enabled Columns" rebuild := func() { // get padding for right alignment of enabled status diff --git a/widgets/menu/main.go b/widgets/menu/main.go index e6f2df1..476bab8 100644 --- a/widgets/menu/main.go +++ b/widgets/menu/main.go @@ -11,13 +11,14 @@ type Padding [2]int // x,y padding type Menu struct { ui.Block SortItems bool // enable automatic sorting of menu items + Selectable bool // whether menu is navigable SubText string // optional text to display before items TextFgColor ui.Attribute TextBgColor ui.Attribute - Selectable bool cursorPos int items Items padding Padding + toolTip *ToolTip } func NewMenu() *Menu { @@ -71,13 +72,9 @@ func (m *Menu) SetCursor(s string) (success bool) { return false } -// Sort menu items(if enabled) and re-calculate window size -func (m *Menu) refresh() { - if m.SortItems { - sort.Sort(m.items) - } - m.calcSize() - ui.Render(m) +// SetToolTip sets an optional tooltip string to show at bottom of screen +func (m *Menu) SetToolTip(lines ...string) { + m.toolTip = NewToolTip(lines...) } func (m *Menu) SelectedItem() Item { @@ -117,6 +114,10 @@ func (m *Menu) Buffer() ui.Buffer { } } + if m.toolTip != nil { + buf.Merge(m.toolTip.Buffer()) + } + return buf } @@ -134,6 +135,15 @@ func (m *Menu) Down() { } } +// Sort menu items(if enabled) and re-calculate window size +func (m *Menu) refresh() { + if m.SortItems { + sort.Sort(m.items) + } + m.calcSize() + ui.Render(m) +} + // Set width and height based on menu items func (m *Menu) calcSize() { m.Width = 7 // minimum width diff --git a/widgets/menu/tooltip.go b/widgets/menu/tooltip.go new file mode 100644 index 0000000..1909ede --- /dev/null +++ b/widgets/menu/tooltip.go @@ -0,0 +1,55 @@ +package menu + +import ( + ui "github.com/gizak/termui" +) + +type ToolTip struct { + ui.Block + Lines []string + TextFgColor ui.Attribute + TextBgColor ui.Attribute + padding Padding +} + +func NewToolTip(lines ...string) *ToolTip { + t := &ToolTip{ + Block: *ui.NewBlock(), + Lines: lines, + TextFgColor: ui.ThemeAttr("menu.text.fg"), + TextBgColor: ui.ThemeAttr("menu.text.bg"), + padding: Padding{2, 1}, + } + t.BorderFg = ui.ThemeAttr("menu.border.fg") + t.BorderLabelFg = ui.ThemeAttr("menu.label.fg") + t.X = 1 + t.Align() + return t +} + +func (t *ToolTip) Buffer() ui.Buffer { + var cell ui.Cell + buf := t.Block.Buffer() + + y := t.Y + t.padding[1] + + for n, line := range t.Lines { + x := t.X + t.padding[0] + for _, ch := range line { + cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor} + buf.Set(x, y+n, cell) + x++ + } + } + + return buf +} + +// Set width and height based on screen size +func (t *ToolTip) Align() { + t.Width = ui.TermWidth() - (t.padding[0] * 2) + t.Height = len(t.Lines) + (t.padding[1] * 2) + t.Y = ui.TermHeight() - t.Height + + t.Block.Align() +}