summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md93
-rw-r--r--cmd/gotop/main.go23
-rw-r--r--config.go3
-rw-r--r--go.sum12
-rw-r--r--layout/layout.go54
-rw-r--r--layout/layout_test.go27
-rw-r--r--layouts/kitchensink4
-rw-r--r--layouts/many_columns_test4
-rw-r--r--termui/gauge.go22
-rw-r--r--widgets/battery.go27
-rw-r--r--widgets/batterygauge.go77
-rw-r--r--widgets/cpu.go42
-rw-r--r--widgets/disk.go24
-rw-r--r--widgets/mem.go30
-rw-r--r--widgets/mem_freebsd.go3
-rw-r--r--widgets/mem_other.go3
-rw-r--r--widgets/net.go23
-rw-r--r--widgets/proc.go4
-rw-r--r--widgets/temp.go19
20 files changed, 439 insertions, 56 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 521e9b8..f4468be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [3.3.2] - ??
+- Fixes #15, crash caused by battery widget when some accessories have batteries
- Fixes #57, colors with dashes in the name not found.
- Also, cjbassi/gotop#127 and cjbassi/gotop#130 were released back in v3.1.0.
diff --git a/README.md b/README.md
index ca7be96..0a47092 100644
--- a/README.md
+++ b/README.md
@@ -111,12 +111,12 @@ and these are separated by spaces.
1. Each line is a row
2. Empty lines are skipped
3. Spaces are compressed (so you can do limited visual formatting)
-4. Legal widget names are: cpu, disk, mem, temp, batt, net, procs
+4. Legal widget names are: cpu, disk, mem, temp, batt, net, procs, power
5. Widget names are not case sensitive
4. The simplest row is a single widget, by name, e.g.
- ```
- cpu
- ```
+ ```
+ cpu
+ ```
5. **Weights**
1. Widgets with no weights have a weight of 1.
2. If multiple widgets are put on a row with no weights, they will all have
@@ -134,19 +134,19 @@ and these are separated by spaces.
second will be 5/7 ~= 67% wide (or, memory will be twice as wide as disk).
9. If prefixed by a number and colon, the widget will span that number of
rows downward. E.g.
- ```
- mem 2:cpu
- net
- ```
+ ```
+ mem 2:cpu
+ net
+ ```
Here, memory and network will be in the same row as CPU, one over the other,
and each half as high as CPU; it'll look like this:
- ```
- +------+------+
- | Mem | |
- +------+ CPU |
- | Net | |
- +------+------+
- ```
+ ```
+ +------+------+
+ | Mem | |
+ +------+ CPU |
+ | Net | |
+ +------+------+
+ ```
10. Negative, 0, or non-integer weights will be recorded as "1". Same for row
spans.
11. Unrecognized widget names will cause the application to abort.
@@ -163,23 +163,71 @@ and these are separated by spaces.
Yes, you're clever enough to break the layout algorithm, but if you try to
build massive edifices, you're in for disappointment.
+### Metrics
+
+gotop can export widget data as Prometheus metrics. This allows users to take
+snapshots of the current state of a machine running gotop, or to query gotop
+remotely.
+
+All metrics are in the `gotop` namespace, and are tagged with
+`goto_<widget>_<enum>`. Metrics are only exported for widgets
+that are enabled, and are updated with the same frequency as the configured
+update interval. Most widgets are exported as Prometheus gauges.
+
+Metrics are disabled by default, and must be enabled with the `--export` flag.
+The flag takes an interface port in the idiomatic Go format of
+`<addy>:<port>`; a common pattern is `-x :2112`. There is **no security**
+on this feature; I recommend that you run this bound to a localhost interface,
+e.g. `127.0.0.1:7653`, and if you want to access this remotely, run it behind
+a proxy that provides SSL and authentication such as
+[Caddy](https://caddyserver.com).
+
+Once enabled, any widgets that are enabled will appear in the HTTP payload of
+a call to `http://<addy>:<port>/metrics`. For example,
+
+```
+➜ ~ curl -s http://localhost:2112/metrics | egrep -e '^gotop'
+gotop_battery_0 0.6387792286668692
+gotop_cpu_0 12.871287128721228
+gotop_cpu_1 11.000000000001364
+gotop_disk_:dev:nvme0n1p1 0.63
+gotop_memory_main 49.932259713701434
+gotop_memory_swap 0
+gotop_net_recv 129461
+gotop_net_sent 218525
+gotop_temp_coretemp_core0 37
+gotop_temp_coretemp_core1 37
+```
+
+Disk metrics are reformatted to replace `/` with `:` which makes them legal
+Prometheus names:
+
+```
+➜ ~ curl -s http://localhost:2112/metrics | egrep -e '^gotop_disk' | tr ':' '/'
+gotop_disk_/dev/nvme0n1p1 0.63
+```
+
+This feature satisfies a ticket request to provide a "snapshot" for comparison
+with a known state, but it is also foundational for a future feature where
+widgets can be configured with virtual devices fed by data from remote gotop
+instances. The objective for that feature is to allow monitoring of multiple
+remote VMs without having to have a wall of gotops running on a large monitor.
+
### CLI Options
`-c`, `--color=NAME` Set a colorscheme.
-`-m`, `--minimal` Only show CPU, Mem and Process widgets. (DEPRECATED for `-l minimal`)
+`-m`, `--minimal` Only show CPU, Mem and Process widgets. (DEPRECATED for `-l minimal`)
`-r`, `--rate=RATE` Number of times per second to update CPU and Mem widgets [default: 1].
`-V`, `--version` Print version and exit.
`-p`, `--percpu` Show each CPU in the CPU widget.
`-a`, `--averagecpu` Show average CPU in the CPU widget.
`-f`, `--fahrenheit` Show temperatures in fahrenheit.
`-s`, `--statusbar` Show a statusbar with the time.
-`-b`, `--battery` Show battery level widget (`minimal` turns off). [preview](./assets/screenshots/battery.png) (DEPRECATED for `-l battery`)
-`-i`, `--interface=NAME` Select network interface [default: all].
-`-l`, `--layout=NAME` Choose a layout. gotop searches for a file by NAME in \$XDG_CONFIG_HOME/gotop, then relative to the current path. "-" reads a layout from stdin, allowing for simple, one-off layouts such as `echo net | gotop -l -`
-
-Several interfaces can be defined using comma separated values.
+`-b`, `--battery` Show battery level widget (`minimal` turns off). [preview](./assets/screenshots/battery.png) (DEPRECATED for `-l battery`)
+`-i`, `--interface=NAME` Select network interface. Several interfaces can be defined using comma separated values. Interfaces can also be ignored by prefixing the interface with `!` [default: all].
+`-l`, `--layout=NAME` Choose a layout. gotop searches for a file by NAME in \$XDG_CONFIG_HOME/gotop, then relative to the current path. "-" reads a layout from stdin, allowing for simple, one-off layouts such as `echo net | gotop -l -`
+`-x`, `--export=PORT` Enable metrics for export on the specified port. This feature is disabled by default.
-Interfaces can also be ignored using `!`
## Built With
@@ -189,3 +237,4 @@ Interfaces can also be ignored using `!`
- [shirou/gopsutil](https://github.com/shirou/gopsutil)
- [goreleaser/nfpm](https://github.com/goreleaser/nfpm)
- [distatus/battery](https://github.com/distatus/battery)
+- [prometheus/client_golang](https://github.com/prometheus/client_golang)
diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go
index 7575655..c708b39 100644
--- a/cmd/gotop/main.go
+++ b/cmd/gotop/main.go
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"log"
+ "net/http"
"os"
"os/signal"
"path/filepath"
@@ -14,6 +15,7 @@ import (
docopt "github.com/docopt/docopt.go"
ui "github.com/gizak/termui/v3"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/xxxserxxx/gotop"
"github.com/xxxserxxx/gotop/colorschemes"
@@ -25,7 +27,7 @@ import (
const (
appName = "gotop"
- version = "3.3.1"
+ version = "3.4.0"
graphHorizontalScaleDelta = 3
defaultUI = "cpu\ndisk/1 2:mem/2\ntemp\nnet procs"
@@ -41,6 +43,7 @@ var (
stderrLogger = log.New(os.Stderr, "", 0)
)
+// TODO: Add tab completion for Linux https://gist.github.com/icholy/5314423
// TODO: state:merge #135 linux console font (cmatsuoka/console-font)
// TODO: state:deferred 157 FreeBSD fixes & Nvidia GPU support (kraust/master). Significant CPU use impact for NVidia changes.
// TODO: Virtual devices from Prometeus metrics @feature
@@ -64,6 +67,7 @@ Options:
-B, --bandwidth=bits Specify the number of bits per seconds.
-l, --layout=NAME Name of layout spec file for the UI. Looks first in $XDG_CONFIG_HOME/gotop, then as a path. Use "-" to pipe.
-i, --interface=NAME Select network interface [default: all].
+ -x, --export=PORT Enable metrics for export on the specified port.
Several interfaces can be defined using comma separated values.
@@ -115,8 +119,11 @@ Colorschemes:
if args["--minimal"].(bool) {
conf.Layout = "minimal"
}
- if val, _ := args["--statusbar"]; val != nil {
- rateStr, _ := args["--rate"].(string)
+ if val, _ := args["--export"]; val != nil {
+ conf.ExportPort = val.(string)
+ }
+ if val, _ := args["--rate"]; val != nil {
+ rateStr, _ := val.(string)
rate, err := strconv.ParseFloat(rateStr, 64)
if err != nil {
return fmt.Errorf("invalid rate parameter")
@@ -335,7 +342,7 @@ func makeConfig() gotop.Config {
HelpVisible: false,
UpdateInterval: time.Second,
AverageLoad: false,
- PercpuLoad: false,
+ PercpuLoad: true,
TempScale: w.Celsius,
Statusbar: false,
NetInterface: w.NET_INTERFACE_ALL,
@@ -345,6 +352,7 @@ func makeConfig() gotop.Config {
return conf
}
+// TODO: mpd visualizer widget
func main() {
// Set up default config
conf := makeConfig()
@@ -400,6 +408,12 @@ func main() {
ui.Render(bar)
}
+ if conf.ExportPort != "" {
+ go func() {
+ http.Handle("/metrics", promhttp.Handler())
+ http.ListenAndServe(conf.ExportPort, nil)
+ }()
+ }
eventLoop(conf, grid)
}
@@ -414,7 +428,6 @@ func getLayout(conf gotop.Config) io.Reader {
case "battery":
return strings.NewReader(batteryUI)
default:
- log.Printf("layout = %s", conf.Layout)
fp := filepath.Join(conf.ConfigDir, conf.Layout)
fin, err := os.Open(fp)
if err != nil {
diff --git a/config.go b/config.go
index 7fe2d72..fc6e521 100644
--- a/config.go
+++ b/config.go
@@ -30,6 +30,7 @@ type Config struct {
NetInterface string
Layout string
MaxLogSize int64
+ ExportPort string
}
func Parse(in io.Reader, conf *Config) error {
@@ -109,6 +110,8 @@ func Parse(in io.Reader, conf *Config) error {
return err
}
conf.MaxLogSize = int64(iv)
+ case "export":
+ conf.ExportPort = kv[1]
}
}
diff --git a/go.sum b/go.sum
index aaabcf8..2a5ea56 100644
--- a/go.sum
+++ b/go.sum
@@ -14,15 +14,11 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cjbassi/drawille-go v0.0.0-20190126131713-27dc511fe6fd h1:XtfPmj9tQRilnrEmI1HjQhxXWRhEM+m8CACtaMJE/kM=
github.com/cjbassi/drawille-go v0.0.0-20190126131713-27dc511fe6fd/go.mod h1:vjcQJUZJYD3MeVGhtZXSMnCHfUNZxsyYzJt90eCYxK4=
-github.com/cjbassi/drawille-go v0.1.0/go.mod h1:vjcQJUZJYD3MeVGhtZXSMnCHfUNZxsyYzJt90eCYxK4=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distatus/battery v0.9.0 h1:8NS5o00/j3Oh2xgocA6pQROTp5guoR+s8CZlWzHC4QM=
github.com/distatus/battery v0.9.0/go.mod h1:gGO7GxHTi1zlRT+cAj8uGG0/8HFiqAeH0TJvoipnuPs=
-github.com/distatus/battery v0.10.0 h1:YbizvmV33mqqC1fPCAEaQGV3bBhfYOfM+2XmL+mvt5o=
-github.com/distatus/battery v0.10.0/go.mod h1:STnSvFLX//eEpkaN7qWRxCWxrWOcssTDgnG4yqq9BRE=
github.com/docopt/docopt.go v0.0.0-20180111231733-ee0de3bc6815 h1:HMAfwOa33y82IaQEKQDfUCiwNlxtM1iw7HLM9ru0RNc=
github.com/docopt/docopt.go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:l7JNRynTRuqe45tpIyItHNqZWTxywYjp87MWTOnU5cg=
github.com/gizak/termui/v3 v3.0.0 h1:NYTUG6ig/sJK05O5FyhWemwlVPO8ilNpvS/PgRtrKAE=
@@ -55,9 +51,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@@ -99,9 +93,6 @@ github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLk
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/shirou/gopsutil v2.18.11+incompatible h1:PMFTKnFTr/YTRW5rbLK4vWALV3a+IGXse5nvhSjztmg=
github.com/shirou/gopsutil v2.18.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
-github.com/shirou/gopsutil v2.20.1+incompatible h1:oIq9Cq4i84Hk8uQAUOG3eNdI/29hBawGrD5YRl6JRDY=
-github.com/shirou/gopsutil v2.20.1+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
-github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -112,7 +103,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 h1:Pn8fQdvx+z1avAi7fdM2kRYWQNxGlavNDSyzrQg2SsU=
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -135,11 +125,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
diff --git a/layout/layout.go b/layout/layout.go
index 3214b97..0ab6b54 100644
--- a/layout/layout.go
+++ b/layout/layout.go
@@ -31,17 +31,17 @@ var widgetNames []string = []string{"cpu", "disk", "mem", "temp", "net", "procs"
func Layout(wl layout, c gotop.Config) (*MyGrid, error) {
rowDefs := wl.Rows
- uiRows := make([]ui.GridItem, 0)
+ uiRows := make([][]interface{}, 0)
numRows := countNumRows(wl.Rows)
- var uiRow ui.GridItem
+ var uiRow []interface{}
for len(rowDefs) > 0 {
uiRow, rowDefs = processRow(c, numRows, rowDefs)
uiRows = append(uiRows, uiRow)
}
rgs := make([]interface{}, 0)
+ rh := 1.0 / float64(len(uiRows))
for _, ur := range uiRows {
- ur.HeightRatio = ur.HeightRatio / float64(numRows)
- rgs = append(rgs, ur)
+ rgs = append(rgs, ui.NewRow(rh, ur...))
}
grid := &MyGrid{ui.NewGrid(), nil, nil}
grid.Set(rgs...)
@@ -58,10 +58,10 @@ func Layout(wl layout, c gotop.Config) (*MyGrid, error) {
// 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) (ui.GridItem, [][]widgetRule) {
+func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) ([]interface{}, [][]widgetRule) {
// Recursive function #3. See the comment in deepFindProc.
if len(rowDefs) < 1 {
- return ui.GridItem{}, [][]widgetRule{}
+ return 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
@@ -86,6 +86,7 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte
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. wid1/2 wid2/2. This block checks for that and, if it occurs,
@@ -101,16 +102,25 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte
rowDefs = append(processing[i:], rowDefs...)
break
}
- // Not all rows have been consumed, so go ahead and place the row's widgets in columns
- for _, wid := range row {
- for j, ch := range colHeights {
- if ch+wid.Height <= maxHeight {
- widget := makeWidget(c, wid)
- columns[j] = append(columns[j], ui.NewRow(float64(wid.Height)/float64(maxHeight), widget))
- colHeights[j] += wid.Height
+ // 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 = processing[i:]
+ break outer
+ }
}
}
var uiColumns []interface{}
@@ -120,11 +130,15 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte
}
}
- return ui.NewRow(1.0/float64(numRows), uiColumns...), rowDefs
+ return uiColumns, rowDefs
+}
+
+type Metric interface {
+ EnableMetric()
}
func makeWidget(c gotop.Config, widRule widgetRule) interface{} {
- var w interface{}
+ var w Metric
switch widRule.Widget {
case "cpu":
cpu := widgets.NewCpuWidget(c.UpdateInterval, c.GraphHorizontalScale, c.AverageLoad, c.PercpuLoad)
@@ -145,7 +159,8 @@ func makeWidget(c gotop.Config, widRule widgetRule) interface{} {
}
w = cpu
case "disk":
- w = widgets.NewDiskWidget()
+ dw := widgets.NewDiskWidget()
+ w = dw
case "mem":
m := widgets.NewMemWidget(c.UpdateInterval, c.GraphHorizontalScale)
m.LineColors["Main"] = ui.Color(c.Colorscheme.MainMem)
@@ -185,10 +200,17 @@ func makeWidget(c gotop.Config, widRule widgetRule) interface{} {
i++
}
w = b
+ case "power":
+ b := widgets.NewBatteryGauge()
+ b.BarColor = ui.Color(c.Colorscheme.ProcCursor)
+ w = b
default:
log.Printf("Invalid widget name %s. Must be one of %v", widRule.Widget, widgetNames)
return ui.NewBlock()
}
+ if c.ExportPort != "" {
+ w.EnableMetric()
+ }
return w
}
diff --git a/layout/layout_test.go b/layout/layout_test.go
index d1f016d..568bbee 100644
--- a/layout/layout_test.go
+++ b/layout/layout_test.go
@@ -101,6 +101,33 @@ func TestParsing(t *testing.T) {
assert.Equal(t, 1, l.Rows[1][0].Height)
assert.Equal(t, 1.0, l.Rows[1][0].Weight)
}},
+ {"cpu/2 mem/1 6:procs\n3:temp/1 2:disk/2\npower\nnet procs", func(l layout) {
+ assert.Equal(t, 4, len(l.Rows))
+ // First row
+ assert.Equal(t, 3, len(l.Rows[0]))
+ assert.Equal(t, 1, l.Rows[0][0].Height)
+ assert.Equal(t, 0.5, l.Rows[0][0].Weight)
+ assert.Equal(t, 1, l.Rows[0][1].Height)
+ assert.Equal(t, 0.25, l.Rows[0][1].Weight)
+ assert.Equal(t, 6, l.Rows[0][2].Height)
+ assert.Equal(t, 0.25, l.Rows[0][2].Weight)
+ // Second row
+ assert.Equal(t, 2, len(l.Rows[1]))
+ assert.Equal(t, 3, l.Rows[1][0].Height)
+ assert.Equal(t, 1/3.0, l.Rows[1][0].Weight)
+ assert.Equal(t, 2, l.Rows[1][1].Height)
+ assert.Equal(t, 2/3.0, l.Rows[1][1].Weight)
+ // Third row
+ assert.Equal(t, 1, len(l.Rows[2]))
+ assert.Equal(t, 1, l.Rows[2][0].Height)
+ assert.Equal(t, 1.0, l.Rows[2][0].Weight)
+ // Fourth row
+ assert.Equal(t, 2, len(l.Rows[3]))
+ assert.Equal(t, 1, l.Rows[3][0].Height)
+ assert.Equal(t, 0.5, l.Rows[3][0].Weight)
+ assert.Equal(t, 1, l.Rows[3][1].Height)
+ assert.Equal(t, 0.5, l.Rows[3][1].Weight)
+ }},
}
for _, tc := range tests {
diff --git a/layouts/kitchensink b/layouts/kitchensink
new file mode 100644
index 0000000..5e25894
--- /dev/null
+++ b/layouts/kitchensink
@@ -0,0 +1,4 @@
+cpu/2 mem/1
+3:temp/1 2:disk/2
+ power
+net procs
diff --git a/layouts/many_columns_test b/layouts/many_columns_test
new file mode 100644
index 0000000..f2d57e3
--- /dev/null
+++ b/layouts/many_columns_test
@@ -0,0 +1,4 @@
+cpu/2 mem/1 6:procs/2
+3:temp/1 2:disk/2
+power
+net procs
diff --git a/termui/gauge.go b/termui/gauge.go
new file mode 100644
index 0000000..db9a9c0
--- /dev/null
+++ b/termui/gauge.go
@@ -0,0 +1,22 @@
+package termui
+
+import (
+ . "github.com/gizak/termui/v3"
+ gizak "github.com/gizak/termui/v3/widgets"
+)
+
+// LineGraph implements a line graph of data points.
+type Gauge struct {
+ *gizak.Gauge
+}
+
+func NewGauge() *Gauge {
+ return &Gauge{
+ Gauge: gizak.NewGauge(),
+ }
+}
+
+func (self *Gauge) Draw(buf *Buffer) {
+ self.Gauge.Draw(buf)
+ self.Gauge.SetRect(self.Min.X, self.Min.Y, self.Inner.Dx(), self.Inner.Dy())
+}
diff --git a/widgets/battery.go b/widgets/battery.go
index 00764c0..b47cac4 100644
--- a/widgets/battery.go
+++ b/widgets/battery.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/distatus/battery"
+ "github.com/prometheus/client_golang/prometheus"
ui "github.com/xxxserxxx/gotop/termui"
)
@@ -15,6 +16,7 @@ import (
type BatteryWidget struct {
*ui.LineGraph
updateInterval time.Duration
+ metric []prometheus.Gauge
}
func NewBatteryWidget(horizontalScale int) *BatteryWidget {
@@ -41,6 +43,25 @@ func NewBatteryWidget(horizontalScale int) *BatteryWidget {
return self
}
+func (b *BatteryWidget) EnableMetric() {
+ bats, err := battery.GetAll()
+ if err != nil {
+ log.Printf("error setting up metrics: %v", err)
+ return
+ }
+ b.metric = make([]prometheus.Gauge, len(bats))
+ for i, bat := range bats {
+ gauge := prometheus.NewGauge(prometheus.GaugeOpts{
+ Namespace: "gotop",
+ Subsystem: "battery",
+ Name: fmt.Sprintf("%d", i),
+ })
+ gauge.Set(bat.Current / bat.Full)
+ b.metric[i] = gauge
+ prometheus.MustRegister(gauge)
+ }
+}
+
func makeId(i int) string {
return "Batt" + strconv.Itoa(i)
}
@@ -74,8 +95,12 @@ func (self *BatteryWidget) update() {
}
for i, battery := range batteries {
id := makeId(i)
- percentFull := math.Abs(battery.Current/battery.Full) * 100.0
+ perc := battery.Current / battery.Full
+ percentFull := math.Abs(perc) * 100.0
self.Data[id] = append(self.Data[id], percentFull)
self.Labels[id] = fmt.Sprintf("%3.0f%% %.0f/%.0f", percentFull, math.Abs(battery.Current), math.Abs(battery.Full))
+ if self.metric != nil {
+ self.metric[i].Set(perc)
+ }
}
}
diff --git a/widgets/batterygauge.go b/widgets/batterygauge.go
new file mode 100644
index 0000000..04b1434
--- /dev/null
+++ b/widgets/batterygauge.go
@@ -0,0 +1,77 @@
+package widgets
+
+import (
+ "fmt"
+ "log"
+ //"math"
+ //"strconv"
+ "time"
+
+ "github.com/distatus/battery"
+ "github.com/prometheus/client_golang/prometheus"
+
+ . "github.com/xxxserxxx/gotop/termui"
+)
+
+type BatteryGauge struct {
+ *Gauge
+ metric prometheus.Gauge
+}
+
+func NewBatteryGauge() *BatteryGauge {
+ self := &BatteryGauge{Gauge: NewGauge()}
+ self.Title = " Power Level "
+
+ self.update()
+
+ go func() {
+ for range time.NewTicker(time.Second).C {
+ self.Lock()
+ self.update()
+ self.Unlock()
+ }
+ }()
+
+ return self
+}
+
+func (b *BatteryGauge) EnableMetric() {
+ bats, err := battery.GetAll()
+ if err != nil {
+ log.Printf("error setting up metrics: %v", err)
+ return
+ }
+ mx := 0.0
+ cu := 0.0
+ for _, bat := range bats {
+ mx += bat.Full
+ cu += bat.Current
+ gauge := prometheus.NewGauge(prometheus.GaugeOpts{
+ Namespace: "gotop",
+ Subsystem: "battery",
+ Name: "total",
+ })
+ gauge.Set(cu / mx)
+ b.metric = gauge
+ prometheus.MustRegister(gauge)
+ }
+}
+
+func (self *BatteryGauge) update() {
+ bats, err := battery.GetAll()
+ if err != nil {
+ log.Printf("error setting up metrics: %v", err)
+ return
+ }
+ mx := 0.0
+ cu := 0.0
+ for _, bat := range bats {
+ mx += bat.Full
+ cu += bat.Current
+ }
+ self.Percent = int((cu / mx) * 100.0)
+ self.Label = fmt.Sprintf("%d%%", self.Percent)
+ if self.metric != nil {
+ self.metric.Set(cu / mx)
+ }
+}
diff --git a/widgets/cpu.go b/widgets/cpu.go
index 8e8819c..37c4220 100644
--- a/widgets/cpu.go
+++ b/widgets/cpu.go
@@ -1,11 +1,13 @@
package widgets
import (
+ "context"
"fmt"
"log"
"sync"
"time"
+ "github.com/prometheus/client_golang/prometheus"
psCpu "github.com/shirou/gopsutil/cpu"
ui "github.com/xxxserxxx/gotop/termui"
@@ -19,6 +21,7 @@ type CpuWidget struct {
updateInterval time.Duration
formatString string
updateLock sync.Mutex
+ metric []prometheus.Gauge
}
func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverageLoad bool, showPerCpuLoad bool) *CpuWidget {
@@ -71,6 +74,35 @@ func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverage
return self
}
+func (self *CpuWidget) EnableMetric() {
+ if self.ShowAverageLoad {
+ self.metric = make([]prometheus.Gauge, 1)
+ self.metric[0] = prometheus.NewGauge(prometheus.GaugeOpts{
+ Subsystem: "cpu",
+ Name: "avg",
+ })
+ } else {
+ ctx, ccl := context.WithTimeout(context.Background(), time.Second*5)
+ defer ccl()
+ percents, err := psCpu.PercentWithContext(ctx, self.updateInterval, true)
+ if err != nil {
+ log.Printf("error setting up metrics: %v", err)
+ return
+ }
+ self.metric = make([]prometheus.Gauge, self.CpuCount)
+ for i, perc := range percents {
+ gauge := prometheus.NewGauge(prometheus.GaugeOpts{
+ Namespace: "gotop",
+ Subsystem: "cpu",
+ Name: fmt.Sprintf("%d", i),
+ })
+ gauge.Set(perc)
+ prometheus.MustRegister(gauge)
+ self.metric[i] = gauge
+ }
+ }
+}
+
func (b *CpuWidget) Scale(i int) {
b.LineGraph.HorizontalScale = i
}
@@ -88,6 +120,9 @@ func (self *CpuWidget) update() {
defer self.updateLock.Unlock()
self.Data["AVRG"] = append(self.Data["AVRG"], percent[0])
self.Labels["AVRG"] = fmt.Sprintf("%3.0f%%", percent[0])
+ if self.metric != nil {
+ self.metric[0].Set(percent[0])
+ }
}
}()
}
@@ -109,6 +144,13 @@ func (self *CpuWidget) update() {
key := fmt.Sprintf(self.formatString, i)
self.Data[key] = append(self.Data[key], percent)
self.Labels[key] = fmt.Sprintf("%3.0f%%", percent)
+ if self.metric != nil {
+ if self.metric[i] == nil {
+ log.Printf("ERROR: not enough metrics %d", i)
+ } else {
+ self.metric[i].Set(percent)
+ }
+ }
}
}
}
diff --git a/widgets/disk.go b/widgets/disk.go
index d20b078..d1da7a6 100644
--- a/widgets/disk.go
+++ b/widgets/disk.go
@@ -7,6 +7,7 @@ import (
"strings"
"time"
+ "github.com/prometheus/client_golang/prometheus"
psDisk "github.com/shirou/gopsutil/disk"
ui "github.com/xxxserxxx/gotop/termui"
@@ -28,6 +29,7 @@ type DiskWidget struct {
*ui.Table
updateInterval time.Duration
Partitions map[string]*Partition
+ metric map[string]prometheus.Gauge
}
func NewDiskWidget() *DiskWidget {
@@ -60,6 +62,21 @@ func NewDiskWidget() *DiskWidget {
return self
}
+func (self *DiskWidget) EnableMetric() {
+ self.metric = make(map[string]prometheus.Gauge)
+ for key, part := range self.Partitions {
+ gauge := prometheus.NewGauge(prometheus.GaugeOpts{
+ Namespace: "gotop",
+ Subsystem: "disk",
+ Name: strings.ReplaceAll(key, "/", ":"),
+ //Name: strings.Replace(strings.Replace(part.Device, "/dev/", "", -1), "mapper/", "", -1),
+ })
+ gauge.Set(float64(part.UsedPercent) / 100.0)
+ prometheus.MustRegister(gauge)
+ self.metric[key] = gauge
+ }
+}
+
func (self *DiskWidget) update() {
partitions, err := psDisk.Partitions(false)
if err != nil {
@@ -158,5 +175,12 @@ func (self *DiskWidget) update() {
self.Rows[i][3] = partition.Free
self.Rows[i][4] = partition.BytesReadRecently
self.Rows[i][5] = partition.BytesWrittenRecently
+ if self.metric != nil {
+ if self.metric[key] == nil {
+ log.Printf("ERROR: missing metric %s", key)
+ } else {
+ self.metric[key].Set(float64(partition.UsedPercent) / 100.0)
+ }
+ }
}
}
diff --git a/widgets/mem.go b/widgets/mem.go
index d5bd67d..1c85f3b 100644
--- a/widgets/mem.go
+++ b/widgets/mem.go
@@ -5,6 +5,7 @@ import (
"log"
"time"
+ "github.com/prometheus/client_golang/prometheus"
psMem "github.com/shirou/gopsutil/mem"
ui "github.com/xxxserxxx/gotop/termui"
@@ -14,6 +15,8 @@ import (
type MemWidget struct {
*ui.LineGraph
updateInterval time.Duration
+ mainMetric prometheus.Gauge
+ swapMetric prometheus.Gauge
}
type MemoryInfo struct {
@@ -45,6 +48,9 @@ func (self *MemWidget) updateMainMemory() {
Used: mainMemory.Used,
UsedPercent: mainMemory.UsedPercent,
})
+ if self.mainMetric != nil {
+ self.mainMetric.Set(mainMemory.UsedPercent)
+ }
}
}
@@ -73,6 +79,30 @@ fun