xmtop/layout/layout.go

343 lines
8.4 KiB
Go

package layout
import (
"log"
"sort"
"strings"
"github.com/xxxserxxx/lingo/v2"
"github.com/xxxserxxx/gotop/v4"
"github.com/xxxserxxx/gotop/v4/widgets"
ui "github.com/gizak/termui/v3"
)
type layout struct {
Rows [][]widgetRule
}
type widgetRule struct {
Widget string
Weight float64
Height int
}
type MyGrid struct {
*ui.Grid
Lines []widgets.Scalable
Proc *widgets.ProcWidget
Net *widgets.NetWidget
}
var widgetNames []string = []string{"cpu", "disk", "mem", "temp", "net", "procs", "batt"}
var tr lingo.Translations
func Layout(wl layout, c gotop.Config) (*MyGrid, error) {
tr = c.Tr
rowDefs := wl.Rows
uiRows := make([][]interface{}, 0)
numRows := countNumRows(wl.Rows)
var uiRow []interface{}
maxHeight := 0
heights := make([]int, 0)
var h int
for len(rowDefs) > 0 {
h, uiRow, rowDefs = processRow(c, numRows, rowDefs)
maxHeight += h
uiRows = append(uiRows, uiRow)
heights = append(heights, h)
}
rgs := make([]interface{}, 0)
for i, ur := range uiRows {
rh := float64(heights[i]) / float64(maxHeight)
rgs = append(rgs, ui.NewRow(rh, ur...))
}
grid := &MyGrid{ui.NewGrid(), nil, nil, nil}
grid.Set(rgs...)
grid.Lines = deepFindScalable(rgs)
res := deepFindWidget(uiRows, func(gs interface{}) interface{} {
p, ok := gs.(*widgets.ProcWidget)
if ok {
return p
}
return nil
})
grid.Proc, _ = res.(*widgets.ProcWidget)
res = deepFindWidget(uiRows, func(gs interface{}) interface{} {
p, ok := gs.(*widgets.NetWidget)
if ok {
return p
}
return nil
})
grid.Net, _ = res.(*widgets.NetWidget)
return grid, nil
}
// processRow eats a single row from the input list of rows and returns a UI
// row (GridItem) representation of the specification, along with a slice
// without that row.
//
// It does more than that, actually, because it may consume more than one row
// if there's a row span widget in the row; in this case, it'll consume as many
// rows as the largest row span object in the row, and produce an uber-row
// containing all that stuff. It returns a slice without the consumed elements.
func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (int, []interface{}, [][]widgetRule) {
// Recursive function #3. See the comment in deepFindProc.
if len(rowDefs) < 1 {
return 0, nil, [][]widgetRule{}
}
// The height of the tallest widget in this row; the number of rows that
// will be consumed, and the overall height of the row that will be
// produced.
maxHeight := countMaxHeight([][]widgetRule{rowDefs[0]})
var processing [][]widgetRule
if maxHeight < len(rowDefs) {
processing = rowDefs[0:maxHeight]
rowDefs = rowDefs[maxHeight:]
} else {
processing = rowDefs[0:]
rowDefs = [][]widgetRule{}
}
var colWeights []float64
var columns [][]interface{}
numCols := len(processing[0])
if numCols < 1 {
numCols = 1
}
for _, rd := range processing[0] {
colWeights = append(colWeights, rd.Weight)
columns = append(columns, make([]interface{}, 0))
}
colHeights := make([]int, numCols)
outer:
for i, row := range processing {
// A definition may fill up the columns before all rows are consumed,
// e.g. cpu/2 net/2. This block checks for that and, if it occurs,
// prepends the remaining rows to the "remainder" return value.
full := true
for _, ch := range colHeights {
if ch <= maxHeight {
full = false
break
}
}
if full {
rowDefs = append(processing[i:], rowDefs...)
break
}
// Not all rows have been consumed, so go ahead and place the row's
// widgets in columns
for w, widg := range row {
placed := false
for k := w; k < len(colHeights); k++ { // there are enough columns
ch := colHeights[k]
if ch+widg.Height <= maxHeight {
widget := makeWidget(c, widg)
columns[k] = append(columns[k], ui.NewRow(float64(widg.Height)/float64(maxHeight), widget))
colHeights[k] += widg.Height
placed = true
break
}
}
// If all columns are full, break out, return the row, and continue processing
if !placed {
rowDefs = append(processing[i:], rowDefs...)
break outer
}
}
}
var uiColumns []interface{}
for i, widgets := range columns {
if len(widgets) > 0 {
uiColumns = append(uiColumns, ui.NewCol(float64(colWeights[i]), widgets...))
}
}
return maxHeight, uiColumns, rowDefs
}
type Metric interface {
EnableMetric()
}
func makeWidget(c gotop.Config, widRule widgetRule) interface{} {
var w Metric
switch widRule.Widget {
case "disk":
dw := widgets.NewDiskWidget()
w = dw
case "cpu":
cpu := widgets.NewCPUWidget(c.UpdateInterval, c.GraphHorizontalScale, c.AverageLoad, c.PercpuLoad)
assignColors(cpu.Data, c.Colorscheme.CPULines, cpu.LineColors)
w = cpu
case "mem":
m := widgets.NewMemWidget(c.UpdateInterval, c.GraphHorizontalScale)
assignColors(m.Data, c.Colorscheme.MemLines, m.LineColors)
w = m
case "batt":
b := widgets.NewBatteryWidget(c.GraphHorizontalScale)
assignColors(b.Data, c.Colorscheme.BattLines, b.LineColors)
w = b
case "temp":
t := widgets.NewTempWidget(c.TempScale, c.Temps)
t.TempLowColor = ui.Color(c.Colorscheme.TempLow)
t.TempHighColor = ui.Color(c.Colorscheme.TempHigh)
w = t
case "net":
n := widgets.NewNetWidget(c.NetInterface)
n.Lines[0].LineColor = ui.Color(c.Colorscheme.Sparklines[0])
n.Lines[0].TitleColor = ui.Color(c.Colorscheme.BorderLabel)
n.Lines[1].LineColor = ui.Color(c.Colorscheme.Sparklines[1])
n.Lines[1].TitleColor = ui.Color(c.Colorscheme.BorderLabel)
n.Mbps = c.Mbps
w = n
case "procs":
p := widgets.NewProcWidget()
p.CursorColor = ui.Color(c.Colorscheme.ProcCursor)
w = p
case "power":
b := widgets.NewBatteryGauge()
b.BarColor = ui.Color(c.Colorscheme.ProcCursor)
w = b
default:
log.Printf(tr.Value("layout.error.widget", widRule.Widget, strings.Join(widgetNames, ",")))
return ui.NewBlock()
}
if c.ExportPort != "" {
w.EnableMetric()
}
return w
}
func assignColors(data map[string][]float64, colors []int, assign map[string]ui.Color) {
// Make sure the data is always processed in the same order so that
// colors are assigned to devices consistently
keys := make([]string, 0, len(data))
for key := range data {
keys = append(keys, key)
}
sort.Strings(keys)
i := 0 // For looping around if we run out of colors
for _, v := range keys {
if i >= len(colors) {
i = 0
}
assign[v] = ui.Color(colors[i])
i++
}
}
func countNumRows(rs [][]widgetRule) int {
var ttl int
for len(rs) > 0 {
ttl++
line := rs[0]
h := 1
for _, c := range line {
if c.Height > h {
h = c.Height
}
}
if h < len(rs) {
rs = rs[h:]
} else {
break
}
}
return ttl
}
// Counts the height of the window so rows can be proportionally scaled.
func countMaxHeight(rs [][]widgetRule) int {
var ttl int
for len(rs) > 0 {
line := rs[0]
h := 1
for _, c := range line {
if c.Height > h {
h = c.Height
}
}
ttl += h
if h < len(rs) {
rs = rs[h:]
} else {
break
}
}
return ttl
}
// deepFindWidget looks in the UI widget tree for a widget, and returns it if found or nil if not.
func deepFindWidget(gs interface{}, test func(v interface{}) interface{}) interface{} {
// Recursive function #1. Recursion is OK here because the number
// of UI elements, even in a very complex UI, is going to be
// relatively small.
t, ok := gs.(ui.GridItem)
if ok {
return deepFindWidget(t.Entry, test)
}
es, ok := gs.([]ui.GridItem)
if ok {
for _, g := range es {
v := deepFindWidget(g, test)
if v != nil {
return v
}
}
}
fs, ok := gs.([]interface{})
if ok {
for _, g := range fs {
v := deepFindWidget(g, test)
if v != nil {
return v
}
}
}
fs2, ok := gs.([][]interface{})
if ok {
for _, g := range fs2 {
v := deepFindWidget(g, test)
if v != nil {
return v
}
}
}
return test(gs)
}
// deepFindScalable looks in the UI widget tree for Scalable widgets,
// and returns them if found or an empty slice if not.
func deepFindScalable(gs interface{}) []widgets.Scalable {
// Recursive function #1. See the comment in deepFindProc.
t, ok := gs.(ui.GridItem)
if ok {
return deepFindScalable(t.Entry)
}
es, ok := gs.([]ui.GridItem)
rvs := make([]widgets.Scalable, 0)
if ok {
for _, g := range es {
vs := deepFindScalable(g)
rvs = append(rvs, vs...)
}
return rvs
}
fs, ok := gs.([]interface{})
if ok {
for _, g := range fs {
vs := deepFindScalable(g)
rvs = append(rvs, vs...)
}
return rvs
}
p, ok := gs.(widgets.Scalable)
if ok {
rvs = append(rvs, p)
}
return rvs
}