Merge branch 'v3.4.x'

This commit is contained in:
Sean E. Russell 2020-02-28 14:38:32 -06:00
commit c6af0ab404
51 changed files with 987 additions and 452 deletions

View File

@ -23,17 +23,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
a histogram.
- Temp widget displays degree symbol (merged from BartWillems, thanks
also fleaz)
- Support for (device) plugins, and abstracting devices from widgets. This
allows adding functionality without adding bulk.
### Fixed
- Keys not controlling process widget, #59
- The one-column bug, #62
## [3.3.2] - 2020-02-26
Bugfix release.
- Fixes #15, crash caused by battery widget when some accessories have batteries
- Fixes #57, colors with dashes in the name not found.
### Fixed
- #15, crash caused by battery widget when some accessories have batteries
- #57, colors with dashes in the name not found.
- Also, cjbassi/gotop#127 and cjbassi/gotop#130 were released back in v3.1.0.
## [3.3.1] - 2020-02-18

View File

@ -4,9 +4,11 @@ import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"plugin"
"strconv"
"strings"
"syscall"
@ -14,6 +16,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,12 +28,13 @@ import (
const (
appName = "gotop"
version = "3.3.2"
version = "3.4.0"
graphHorizontalScaleDelta = 3
defaultUI = "cpu\ndisk/1 2:mem/2\ntemp\nnet procs"
minimalUI = "cpu\nmem procs"
batteryUI = "cpu/2 batt/1\ndisk/1 2:mem/2\ntemp\nnet procs"
procsUI = "cpu 4:procs\ndisk\nmem\nnet"
)
var (
@ -41,10 +45,10 @@ 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
// TODO: Export Prometheus metrics @feature
// TODO: state:merge #167 configuration file (jrswab/configFile111)
func parseArgs(conf *gotop.Config) error {
usage := `
@ -63,11 +67,16 @@ Options:
-b, --battery Show battery level widget ('minimal' turns off). (DEPRECATED, use -l battery)
-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].
-i, --interface=NAME Select network interface [default: all]. Several interfaces can be defined using comma separated values. Interfaces can also be ignored using !
-x, --export=PORT Enable metrics for export on the specified port.
-X, --extensions=NAMES Enables the listed extensions. This is a comma-separated list without the .so suffix. The current and config directories will be searched.
Several interfaces can be defined using comma separated values.
Interfaces can also be ignored using !
Built-in layouts:
default
minimal
battery
kitchensink
Colorschemes:
default
@ -115,8 +124,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")
@ -136,6 +148,10 @@ Colorschemes:
if val, _ := args["--interface"]; val != nil {
conf.NetInterface, _ = args["--interface"].(string)
}
if val, _ := args["--extensions"]; val != nil {
exs, _ := args["--extensions"].(string)
conf.Extensions = strings.Split(exs, ",")
}
return nil
}
@ -335,7 +351,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 +361,7 @@ func makeConfig() gotop.Config {
return conf
}
// TODO: mpd visualizer widget
func main() {
// Set up default config
conf := makeConfig()
@ -379,6 +396,8 @@ func main() {
bar = w.NewStatusBar()
}
loadExtensions(conf)
lstream := getLayout(conf)
ly := layout.ParseLayout(lstream)
grid, err := layout.Layout(ly, conf)
@ -400,6 +419,12 @@ func main() {
ui.Render(bar)
}
if conf.ExportPort != "" {
go func() {
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(conf.ExportPort, nil)
}()
}
eventLoop(conf, grid)
}
@ -413,8 +438,9 @@ func getLayout(conf gotop.Config) io.Reader {
return strings.NewReader(minimalUI)
case "battery":
return strings.NewReader(batteryUI)
case "procs":
return strings.NewReader(procsUI)
default:
log.Printf("layout = %s", conf.Layout)
fp := filepath.Join(conf.ConfigDir, conf.Layout)
fin, err := os.Open(fp)
if err != nil {
@ -426,3 +452,47 @@ func getLayout(conf gotop.Config) io.Reader {
return fin
}
}
func loadExtensions(conf gotop.Config) {
var hasError bool
for _, ex := range conf.Extensions {
exf := ex + ".so"
fn := exf
_, err := os.Stat(fn)
if err != nil && os.IsNotExist(err) {
log.Printf("no plugin %s found in current directory", fn)
fn = filepath.Join(conf.ConfigDir, exf)
_, err = os.Stat(fn)
if err != nil || os.IsNotExist(err) {
hasError = true
log.Printf("no plugin %s found in config directory", fn)
continue
}
}
p, err := plugin.Open(fn)
if err != nil {
hasError = true
log.Printf(err.Error())
continue
}
init, err := p.Lookup("Init")
if err != nil {
hasError = true
log.Printf(err.Error())
continue
}
initFunc, ok := init.(func())
if !ok {
hasError = true
log.Printf(err.Error())
continue
}
initFunc()
}
if hasError {
ui.Close()
fmt.Printf("Error initializing requested plugins; check the log file %s\n", filepath.Join(conf.ConfigDir, conf.LogFile))
os.Exit(1)
}
}

View File

@ -12,8 +12,7 @@ func init() {
BattLines: []int{4, 3, 2, 1, 5, 6, 7, 8},
MainMem: 5,
SwapMem: 11,
MemLines: []int{5, 11, 4, 3, 2, 1, 6, 7, 8},
ProcCursor: 4,

View File

@ -11,8 +11,7 @@
"BattLines": [4, 3, 2, 1, 5, 6, 7, 8],
"MainMem": 5,
"SwapMem": 11,
"MemLines": [5, 11, 4, 3, 2, 1, 6, 7, 8],
"ProcCursor": 4,

View File

@ -12,8 +12,7 @@ func init() {
BattLines: []int{4, 3, 2, 1, 5, 6, 7, 8},
MainMem: 5,
SwapMem: 3,
MemLines: []int{5, 3, 4, 2, 1, 6, 7, 8, 11},
ProcCursor: 33,

View File

@ -12,8 +12,7 @@ func init() {
BattLines: []int{81, 70, 208, 197, 249, 141, 221, 186},
MainMem: 208,
SwapMem: 186,
MemLines: []int{208, 186, 81, 70, 208, 197, 249, 141, 221, 186},
ProcCursor: 197,

View File

@ -25,8 +25,7 @@ func init() {
BattLines: []int{4, 3, 2, 1, 5, 6, 7, 8},
MainMem: 172, // Orange
SwapMem: 221, // yellow
MemLines: []int{172, 221, 4, 3, 2, 1, 5, 6, 7, 8},
ProcCursor: 31, // blue (nord9)

View File

@ -15,8 +15,7 @@ func init() {
BattLines: []int{61, 33, 37, 64, 125, 160, 166, 136},
MainMem: 125,
SwapMem: 166,
MemLines: []int{125, 166, 61, 33, 37, 64, 125, 160, 166, 136},
ProcCursor: 136,

View File

@ -14,8 +14,7 @@ func init() {
BattLines: []int{13, 4, 6, 2, 5, 1, 9, 3},
MainMem: 5,
SwapMem: 9,
MemLines: []int{5, 9, 13, 4, 6, 2, 1, 3},
ProcCursor: 4,

View File

@ -14,8 +14,7 @@ func init() {
BattLines: []int{13, 4, 6, 2, 5, 1, 9, 3},
MainMem: 5,
SwapMem: 9,
MemLines: []int{5, 9, 13, 4, 6, 2, 1, 3},
ProcCursor: 4,

View File

@ -32,8 +32,7 @@ type Colorscheme struct {
BattLines []int
MainMem int
SwapMem int
MemLines []int
ProcCursor int

View File

@ -12,8 +12,7 @@ func init() {
BattLines: []int{212, 218, 123, 159, 229, 158, 183, 146},
MainMem: 201,
SwapMem: 97,
MemLines: []int{201, 97, 212, 218, 123, 159, 229, 158, 183, 146},
ProcCursor: 159,

View File

@ -30,6 +30,8 @@ type Config struct {
NetInterface string
Layout string
MaxLogSize int64
ExportPort string
Extensions []string
}
func Parse(in io.Reader, conf *Config) error {
@ -109,6 +111,10 @@ func Parse(in io.Reader, conf *Config) error {
return err
}
conf.MaxLogSize = int64(iv)
case "export":
conf.ExportPort = kv[1]
case "extensions":
conf.Extensions = strings.Split(kv[1], ",")
}
}

34
devices/cpu.go Normal file
View File

@ -0,0 +1,34 @@
package devices
import (
"log"
"time"
)
var cpuFuncs []func(map[string]int, time.Duration, bool) map[string]error
// RegisterCPU adds a new CPU device to the CPU widget. labels returns the
// names of the devices; they should be as short as possible, and the indexes
// of the returned slice should align with the values returned by the percents
// function. The percents function should return the percent CPU usage of the
// device(s), sliced over the time duration supplied. If the bool argument to
// percents is true, it is expected that the return slice
//
// labels may be called once and the value cached. This means the number of
// cores should not change dynamically.
func RegisterCPU(f func(map[string]int, time.Duration, bool) map[string]error) {
cpuFuncs = append(cpuFuncs, f)
}
// CPUPercent calculates the percentage of cpu used either per CPU or combined.
// Returns one value per cpu, or a single value if percpu is set to false.
func UpdateCPU(cpus map[string]int, interval time.Duration, logical bool) {
for _, f := range cpuFuncs {
errs := f(cpus, interval, logical)
if errs != nil {
for k, e := range errs {
log.Printf("%s: %s", k, e)
}
}
}
}

31
devices/cpu_cpu.go Normal file
View File

@ -0,0 +1,31 @@
package devices
import (
"fmt"
"time"
psCpu "github.com/shirou/gopsutil/cpu"
)
func init() {
f := func(cpus map[string]int, iv time.Duration, l bool) map[string]error {
cpuCount, err := psCpu.Counts(l)
if err != nil {
return nil
}
formatString := "CPU%1d"
if cpuCount > 10 {
formatString = "CPU%02d"
}
vals, err := psCpu.Percent(iv, l)
if err != nil {
return map[string]error{"gopsutil": err}
}
for i := 0; i < len(vals); i++ {
key := fmt.Sprintf(formatString, i)
cpus[key] = int(vals[i])
}
return nil
}
RegisterCPU(f)
}

26
devices/devices.go Normal file
View File

@ -0,0 +1,26 @@
package devices
import "log"
var shutdownFuncs []func() error
// RegisterShutdown stores a function to be called by gotop on exit, allowing
// extensions to properly release resources. Extensions should register a
// shutdown function IFF the extension is using resources that need to be
// released. The returned error will be logged, but no other action will be
// taken.
func RegisterShutdown(f func() error) {
shutdownFuncs = append(shutdownFuncs, f)
}
// Shutdown will be called by the `main()` function if gotop is exited
// cleanly. It will call all of the registered shutdown functions of devices,
// logging all errors but otherwise not responding to them.
func Shutdown() {
for _, f := range shutdownFuncs {
err := f()
if err != nil {
log.Print(err)
}
}
}

26
devices/mem.go Normal file
View File

@ -0,0 +1,26 @@
package devices
import "log"
var memFuncs []func(map[string]MemoryInfo) map[string]error
type MemoryInfo struct {
Total uint64
Used uint64
UsedPercent float64
}
func RegisterMem(f func(map[string]MemoryInfo) map[string]error) {
memFuncs = append(memFuncs, f)
}
func UpdateMem(mem map[string]MemoryInfo) {
for _, f := range memFuncs {
errs := f(mem)
if errs != nil {
for k, e := range errs {
log.Printf("%s: %s", k, e)
}
}
}
}

21
devices/mem_mem.go Normal file
View File

@ -0,0 +1,21 @@
package devices
import (
psMem "github.com/shirou/gopsutil/mem"
)
func init() {
mf := func(mems map[string]MemoryInfo) map[string]error {
mainMemory, err := psMem.VirtualMemory()
if err != nil {
return map[string]error{"Main": err}
}
mems["Main"] = MemoryInfo{
Total: mainMemory.Total,
Used: mainMemory.Used,
UsedPercent: mainMemory.UsedPercent,
}
return nil
}
RegisterMem(mf)
}

View File

@ -0,0 +1,44 @@
// +build freebsd
package devices
import (
"os/exec"
"strconv"
"strings"
)
func init() {
mf := func(mems map[string]MemoryInfo) map[string]error {
cmd := "swapinfo -k|sed -n '1!p'|awk '{print $2,$3,$5}'"
output, err := exec.Command("sh", "-c", cmd).Output()
if err != nil {
return map[string]error{"swapinfo": err}
}
s := strings.TrimSuffix(string(output), "\n")
s = strings.ReplaceAll(s, "\n", " ")
ss := strings.Split(s, " ")
ss = ss[((len(ss)/3)-1)*3:]
errors := make(map[string]error)
mem := MemoryInfo{}
mem.Total, err = strconv.ParseUint(ss[0], 10, 64)
if err != nil {
errors["swap total"] = err
}
mem.Used, err = strconv.ParseUint(ss[1], 10, 64)
if err != nil {
errors["swap used"] = err
}
mem.UsedPercent, err = strconv.ParseFloat(strings.TrimSuffix(ss[2], "%"), 64)
if err != nil {
errors["swap percent"] = err
}
mems["Swap"] = mem
return errors
}
RegisterMem(mf)
}

23
devices/mem_swap_other.go Normal file
View File

@ -0,0 +1,23 @@
// +build !freebsd
package devices
import (
psMem "github.com/shirou/gopsutil/mem"
)
func init() {
mf := func(mems map[string]MemoryInfo) map[string]error {
memory, err := psMem.SwapMemory()
if err != nil {
return map[string]error{"Swap": err}
}
mems["Swap"] = MemoryInfo{
Total: memory.Total,
Used: memory.Used,
UsedPercent: memory.UsedPercent,
}
return nil
}
RegisterMem(mf)
}

22
devices/temp.go Normal file
View File

@ -0,0 +1,22 @@
package devices
import (
"log"
)
var tempUpdates []func(map[string]int) map[string]error
func RegisterTemp(update func(map[string]int) map[string]error) {
tempUpdates = append(tempUpdates, update)
}
func UpdateTemps(temps map[string]int) {
for _, f := range tempUpdates {
errs := f(temps)
if errs != nil {
for k, e := range errs {
log.Printf("error updating temp for %s: %s", k, e)
}
}
}
}

View File

@ -1,22 +1,16 @@
// +build darwin
package widgets
package devices
// #cgo LDFLAGS: -framework IOKit
// #include "include/smc.c"
import "C"
import (
"log"
"github.com/xxxserxxx/gotop/utils"
)
type TemperatureStat struct {
SensorKey string `json:"sensorKey"`
Temperature float64 `json:"sensorTemperature"`
func init() {
RegisterTemp(update)
}
func SensorsTemperatures() ([]TemperatureStat, error) {
func update(temps map[string]int) map[string]error {
temperatureKeys := map[string]string{
C.AMBIENT_AIR_0: "ambient_air_0",
C.AMBIENT_AIR_1: "ambient_air_1",
@ -41,34 +35,12 @@ func SensorsTemperatures() ([]TemperatureStat, error) {
C.WIRELESS_MODULE: "wireless_module",
}
var temperatures []TemperatureStat
C.open_smc()
defer C.close_smc()
for key, val := range temperatureKeys {
temperatures = append(temperatures, TemperatureStat{
SensorKey: val,
Temperature: float64(C.get_tmp(C.CString(key), C.CELSIUS)),
})
temps[val] = int(C.get_tmp(C.CString(key), C.CELSIUS))
}
return temperatures, nil
}
func (self *TempWidget) update() {
sensors, err := SensorsTemperatures()
if err != nil {
log.Printf("failed to get sensors from CGO: %v", err)
return
}
for _, sensor := range sensors {
if sensor.Temperature != 0 {
switch self.TempScale {
case Fahrenheit:
self.Data[sensor.SensorKey] = utils.CelsiusToFahrenheit(int(sensor.Temperature))
case Celsius:
self.Data[sensor.SensorKey] = int(sensor.Temperature)
}
}
}
return nil
}

45
devices/temp_freebsd.go Normal file
View File

@ -0,0 +1,45 @@
// +build freebsd
package devices
import (
"os/exec"
"strconv"
"strings"
"github.com/xxxserxxx/gotop/utils"
)
func init() {
RegisterTemp(update)
}
var sensorOIDS = map[string]string{
"dev.cpu.0.temperature": "CPU 0 ",
"hw.acpi.thermal.tz0.temperature": "Thermal zone 0",
}
func update(temps map[string]int) map[string]error {
var errors map[string]error
for k, v := range sensorOIDS {
output, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
errors[v] = err
continue
}
s1 := strings.Replace(string(output), "C", "", 1)
s2 := strings.TrimSuffix(s1, "\n")
convertedOutput := utils.ConvertLocalizedString(s2)
value, err := strconv.ParseFloat(convertedOutput, 64)
if err != nil {
errors[v] = err
continue
}
temps[v] = int(value)
}
return errors
}

View File

@ -1,32 +1,29 @@
// +build linux
package widgets
package devices
import (
"log"
"strings"
psHost "github.com/shirou/gopsutil/host"
"github.com/xxxserxxx/gotop/utils"
)
func (self *TempWidget) update() {
func init() {
RegisterTemp(getTemps)
}
func getTemps(temps map[string]int) map[string]error {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
log.Printf("error received from gopsutil: %v", err)
return map[string]error{"psHost": err}
}
for _, sensor := range sensors {
// only sensors with input in their name are giving us live temp info
if strings.Contains(sensor.SensorKey, "input") && sensor.Temperature != 0 {
// removes '_input' from the end of the sensor name
label := sensor.SensorKey[:strings.Index(sensor.SensorKey, "_input")]
switch self.TempScale {
case Fahrenheit:
self.Data[label] = utils.CelsiusToFahrenheit(int(sensor.Temperature))
case Celsius:
self.Data[label] = int(sensor.Temperature)
}
temps[label] = int(sensor.Temperature)
}
}
return nil
}

View File

@ -1,6 +1,6 @@
// +build openbsd
package widgets
package devices
// loosely based on https://github.com/openbsd/src/blob/master/sbin/sysctl/sysctl.c#L2517
@ -13,42 +13,13 @@ import (
"strconv"
"syscall"
"unsafe"
"github.com/xxxserxxx/gotop/utils"
)
func (self *TempWidget) getTemp(mib []C.int, mlen int, snsrdev *C.struct_sensordev, index int) {
switch mlen {
case 4:
k := mib[3]
var numt C.int
for numt = 0; numt < snsrdev.maxnumt[k]; numt++ {
mib[4] = numt
self.getTemp(mib, mlen+1, snsrdev, int(numt))
}
case 5:
var snsr C.struct_sensor
var slen C.size_t = C.sizeof_struct_sensor
if v, _ := C.sysctl(&mib[0], 5, unsafe.Pointer(&snsr), &slen, nil, 0); v == -1 {
return
}
if slen > 0 && (snsr.flags&C.SENSOR_FINVALID) == 0 {
key := C.GoString(&snsrdev.xname[0]) + ".temp" + strconv.Itoa(index)
temp := int((snsr.value - 273150000.0) / 1000000.0)
switch self.TempScale {
case Fahrenheit:
self.Data[key] = utils.CelsiusToFahrenheit(temp)
case Celsius:
self.Data[key] = temp
}
}
}
func init() {
RegisterTemp(update)
}
func (self *TempWidget) update() {
func update(temps map[string]int) map[string]error {
mib := []C.int{0, 1, 2, 3, 4}
var snsrdev C.struct_sensordev
@ -69,6 +40,33 @@ func (self *TempWidget) update() {
break
}
}
self.getTemp(mib, 4, &snsrdev, 0)
getTemp(temps, mib, 4, &snsrdev, 0)
}
return nil
}
func getTemp(temps map[string]int, mib []C.int, mlen int, snsrdev *C.struct_sensordev, index int) {
switch mlen {
case 4:
k := mib[3]
var numt C.int
for numt = 0; numt < snsrdev.maxnumt[k]; numt++ {
mib[4] = numt
getTemp(temps, mib, mlen+1, snsrdev, int(numt))
}
case 5:
var snsr C.struct_sensor
var slen C.size_t = C.sizeof_struct_sensor
if v, _ := C.sysctl(&mib[0], 5, unsafe.Pointer(&snsr), &slen, nil, 0); v == -1 {
return
}
if slen > 0 && (snsr.flags&C.SENSOR_FINVALID) == 0 {
key := C.GoString(&snsrdev.xname[0]) + ".temp" + strconv.Itoa(index)
temp := int((snsr.value - 273150000.0) / 1000000.0)
temps[key] = temp
}
}
}

24
devices/temp_windows.go Normal file
View File

@ -0,0 +1,24 @@
// +build windows
package devices
import (
psHost "github.com/shirou/gopsutil/host"
)
func init() {
RegisterTemp(update)
}
func update(temps map[string]int) map[string]error {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
return map[string]error{"gopsutil": err}
}
for _, sensor := range sensors {
if sensor.Temperature != 0 {
temps[sensor.SensorKey] = int(sensor.Temperature)
}
}
return nil
}

22
docs/extensions.md Normal file
View File

@ -0,0 +1,22 @@
% Plugins
# Extensions
- Plugins will supply an `Init()` function that will call the appropriate
`Register\*()` functions in the `github.com/xxxserxxx/gotop/devices` package.
- `devices` will supply:
- RegisterCPU (opt)
- Counts (req)
- Percents (req)
- RegisterMem (opt)
- RegisterTemp (opt)
- RegisterShutdown (opt)
# gotop
- Command line -P, comma separated list of plugin .so
- gotop will look in `pwd` and then in \$XDG_CONFIG_HOME/gotop
- When loaded, gotop will call lib#Init()
When exited cleanly, gotop will call all registered shutdown functions.

42
docs/grid-fill.md Normal file
View File

@ -0,0 +1,42 @@
T is max height in row
S(T) is all widgets with height T
R(T) is all widgets with height < T
X is len(R) > 0 ? 1 : 0
C is len(S) + X
Make row
Make C columns
Place S
Recurse with R; place result
1 2 3 4 5
cpu/2............... mem/1. 6:procs/2..........
3:temp/1. 2:disk/2......... |..................
|........ |................ |..................
|........ power/2.......... |..................
net/2............... batt.. |..................
1 2 3 4 5
cpu/2............... 6:procs/2........ mem/1...
2:disk/2............ |................ 3:temp/1
|................... |................ |.......
power/2............. |................ |.......
net/2............... |................ batt
1 2 3 4 5
1x2................. 3x2.............. 1x1..... 221 221
2x2................. ||||||||||||||||| 3x1..... 21 2x1
|||||||||||||||||||| ||||||||||||||||| ||||||||
1x1...... 1x1...... 1x2.............. 1x1..... 1121
1x2................. 1x2.............. |||||||| 22 22x
1x1...... 1x4................................... 14
initial columns = initial row
fill
pattern for row
does pattern fit columns?
yes: place widgets
no: new row w/ new columns; fill
does fit
cw < patt_c_w

37
docs/releasing.md Normal file
View File

@ -0,0 +1,37 @@
Current steps for a release:
### gotop
1. Update Version in main.go
2. Update CHANGELOG.md
3. Tag
4. Push everything
5. ./make.sh
6. Create github release
### Homebrew
1. Change homebrew-gotop
```
curl --output - -L https://github.com/xxxserxxx/gotop/releases/download/v3.3.2/gotop_3.3.2_linux_amd64.tgz | sha256sum
curl --output - -L https://github.com/xxxserxxx/gotop/releases/download/v3.3.2/gotop_3.3.2_darwin_amd64.tgz | sha256sum
```
### AUR
1. Update aur/PKGBUILD
2. namcap PKGBUILD
3. makepkg
4. makepkg -g
5. makepkg --printsrcinfo \> .SRCINFO
6. Commit everything
7. push
```
curl -L https://github.com/xxxserxxx/gotop/archive/v3.3.2.tar.gz | sha256sum
```
### AUR-BIN
1. Update aur-bin/PKGBUILD
2. namcap PKGBUILD
3. makepkg
4. makepkg -g
5. makepkg --printsrcinfo \> .SRCINFO
6. Commit everything
7. push aur-bin

4
go.mod
View File

@ -1,4 +1,4 @@
module github.com/xxxserxxx/gotop
module github.com/xxxserxxx/gotop/v3
require (
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
@ -15,4 +15,4 @@ require (
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
)
go 1.12
go 1.13

32
go.sum
View File

@ -1,7 +1,5 @@
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -14,29 +12,21 @@ 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=
github.com/gizak/termui/v3 v3.0.0/go.mod h1:uinu2dMdtMI+FTIdEFUJQT5y+KShnhQRshvPblXq3lY=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -44,6 +34,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -57,15 +48,11 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
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=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@ -76,9 +63,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be h1:yzmWtPyxEUIKdZg4RcPq64MfS8NA6A5fNOJgYhpR9EQ=
github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -99,20 +85,17 @@ 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 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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=
@ -125,21 +108,18 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
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=

View File

@ -31,17 +31,22 @@ 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{}
maxHeight := 0
heights := make([]int, 0)
var h int
for len(rowDefs) > 0 {
uiRow, rowDefs = processRow(c, numRows, rowDefs)
h, uiRow, rowDefs = processRow(c, numRows, rowDefs)
maxHeight += h
uiRows = append(uiRows, uiRow)
heights = append(heights, h)
}
rgs := make([]interface{}, 0)
for _, ur := range uiRows {
ur.HeightRatio = ur.HeightRatio / float64(numRows)
rgs = append(rgs, ur)
for i, ur := range uiRows {
rh := float64(heights[i]) / float64(maxHeight)
rgs = append(rgs, ui.NewRow(rh, ur...))
}
grid := &MyGrid{ui.NewGrid(), nil, nil}
grid.Set(rgs...)
@ -58,10 +63,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) (int, []interface{}, [][]widgetRule) {
// Recursive function #3. See the comment in deepFindProc.
if len(rowDefs) < 1 {
return ui.GridItem{}, [][]widgetRule{}
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
@ -86,9 +91,10 @@ 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,
// 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 {
@ -101,16 +107,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 = append(processing[i:], rowDefs...)
break outer
}
}
}
var uiColumns []interface{}
@ -120,11 +135,15 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte
}
}
return ui.NewRow(1.0/float64(numRows), uiColumns...), rowDefs
return maxHeight, 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,11 +164,19 @@ 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)
m.LineColors["Swap"] = ui.Color(c.Colorscheme.SwapMem)
var i int
for key, _ := range m.Data {
if i >= len(c.Colorscheme.MemLines) {
i = 0
}
color := c.Colorscheme.MemLines[i]
m.LineColors[key] = ui.Color(color)
i++
}
w = m
case "temp":
t := widgets.NewTempWidget(c.TempScale)
@ -185,10 +212,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
}

View File

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

4
layouts/kitchensink Normal file
View File

@ -0,0 +1,4 @@
3:cpu/2 3:mem/1
4:temp/1 3:disk/2
power
3:net 3:procs

View File

@ -0,0 +1,4 @@
cpu/2 mem/1 6:procs/2
3:temp/1 2:disk/2
power
net procs

View File

@ -92,6 +92,7 @@ function cdarwinz() {
cd darwin
else
cd darwin
git checkout -- .
git pull
fi
export CGO_ENABLED=1

22
termui/gauge.go Normal file
View File

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

View File

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

86
widgets/batterygauge.go Normal file
View File

@ -0,0 +1,86 @@
package widgets
import (
"fmt"
"log"
"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 batteries: %v", err)
return
}
mx := 0.0
cu := 0.0
charging := "%d%% ⚡%s"
rate := 0.0
for _, bat := range bats {
mx += bat.Full
cu += bat.Current
if rate < bat.ChargeRate {
rate = bat.ChargeRate
}
if bat.State == battery.Charging {
charging = "%d%% 🔌%s"
}
}
tn := (mx - cu) / rate
d, _ := time.ParseDuration(fmt.Sprintf("%fh", tn))
self.Percent = int((cu / mx) * 100.0)
self.Label = fmt.Sprintf(charging, self.Percent, d.Truncate(time.Minute))
if self.metric != nil {
self.metric.Set(cu / mx)
}
}

View File

@ -6,7 +6,8 @@ import (
"sync"
"time"
psCpu "github.com/shirou/gopsutil/cpu"
"github.com/prometheus/client_golang/prometheus"
"github.com/xxxserxxx/gotop/devices"
ui "github.com/xxxserxxx/gotop/termui"
)
@ -17,26 +18,19 @@ type CpuWidget struct {
ShowAverageLoad bool
ShowPerCpuLoad bool
updateInterval time.Duration
formatString string
updateLock sync.Mutex
metric map[string]prometheus.Gauge
}
var cpuLabels []string
func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverageLoad bool, showPerCpuLoad bool) *CpuWidget {
cpuCount, err := psCpu.Counts(false)
if err != nil {
log.Printf("failed to get CPU count from gopsutil: %v", err)
}
formatString := "CPU%1d"
if cpuCount > 10 {
formatString = "CPU%02d"
}
self := &CpuWidget{
LineGraph: ui.NewLineGraph(),
CpuCount: cpuCount,
CpuCount: len(cpuLabels),
updateInterval: updateInterval,
ShowAverageLoad: showAverageLoad,
ShowPerCpuLoad: showPerCpuLoad,
formatString: formatString,
}
self.Title = " CPU Usage "
self.HorizontalScale = horizontalScale
@ -54,9 +48,10 @@ func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverage
}
if self.ShowPerCpuLoad {
for i := 0; i < int(self.CpuCount); i++ {
key := fmt.Sprintf(formatString, i)
self.Data[key] = []float64{0}
cpus := make(map[string]int)
devices.UpdateCPU(cpus, self.updateInterval, self.ShowPerCpuLoad)
for k, v := range cpus {
self.Data[k] = []float64{float64(v)}
}
}
@ -71,6 +66,30 @@ func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverage
return self
}
func (self *CpuWidget) EnableMetric() {
if self.ShowAverageLoad {
self.metric = make(map[string]prometheus.Gauge)
self.metric["AVRG"] = prometheus.NewGauge(prometheus.GaugeOpts{
Subsystem: "cpu",
Name: "avg",
})
} else {
cpus := make(map[string]int)
devices.UpdateCPU(cpus, self.updateInterval, self.ShowPerCpuLoad)
self.metric = make(map[string]prometheus.Gauge)
for key, perc := range cpus {
gauge := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "gotop",
Subsystem: "cpu",
Name: key,
})
gauge.Set(float64(perc))
prometheus.MustRegister(gauge)
self.metric[key] = gauge
}
}
}
func (b *CpuWidget) Scale(i int) {
b.LineGraph.HorizontalScale = i
}
@ -78,37 +97,41 @@ func (b *CpuWidget) Scale(i int) {
func (self *CpuWidget) update() {
if self.ShowAverageLoad {
go func() {
percent, err := psCpu.Percent(self.updateInterval, false)
if err != nil {
log.Printf("failed to get average CPU usage percent from gopsutil: %v. self.updateInterval: %v. percpu: %v", err, self.updateInterval, false)
} else {
self.Lock()
defer self.Unlock()
self.updateLock.Lock()
defer self.updateLock.Unlock()
self.Data["AVRG"] = append(self.Data["AVRG"], percent[0])
self.Labels["AVRG"] = fmt.Sprintf("%3.0f%%", percent[0])
cpus := make(map[string]int)
devices.UpdateCPU(cpus, self.updateInterval, false)
self.Lock()
defer self.Unlock()
self.updateLock.Lock()
defer self.updateLock.Unlock()
var val float64
for _, v := range cpus {
val = float64(v)
break
}
self.Data["AVRG"] = append(self.Data["AVRG"], val)
self.Labels["AVRG"] = fmt.Sprintf("%3.0f%%", val)
if self.metric != nil {
self.metric["AVRG"].Set(val)
}
}()
}
if self.ShowPerCpuLoad {
go func() {
percents, err := psCpu.Percent(self.updateInterval, true)
if err != nil {
log.Printf("failed to get CPU usage percents from gopsutil: %v. self.updateInterval: %v. percpu: %v", err, self.updateInterval, true)
} else {
if len(percents) != int(self.CpuCount) {
log.Printf("error: number of CPU usage percents from gopsutil doesn't match CPU count. percents: %v. self.Count: %v", percents, self.CpuCount)
} else {
self.Lock()
defer self.Unlock()
self.updateLock.Lock()
defer self.updateLock.Unlock()
for i, percent := range percents {
key := fmt.Sprintf(self.formatString, i)
self.Data[key] = append(self.Data[key], percent)
self.Labels[key] = fmt.Sprintf("%3.0f%%", percent)
cpus := make(map[string]int)
devices.UpdateCPU(cpus, self.updateInterval, true)
self.Lock()
defer self.Unlock()
self.updateLock.Lock()
defer self.updateLock.Unlock()
for key, percent := range cpus {
self.Data[key] = append(self.Data[key], float64(percent))
self.Labels[key] = fmt.Sprintf("%d%%", percent)
if self.metric != nil {
if self.metric[key] == nil {
log.Printf("no metrics for %s", key)
} else {
self.metric[key].Set(float64(percent))
}
}
}

View File

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

View File

@ -5,8 +5,9 @@ import (
"log"
"time"
psMem "github.com/shirou/gopsutil/mem"
"github.com/prometheus/client_golang/prometheus"
"github.com/xxxserxxx/gotop/devices"
ui "github.com/xxxserxxx/gotop/termui"
"github.com/xxxserxxx/gotop/utils"
)
@ -14,15 +15,62 @@ import (
type MemWidget struct {
*ui.LineGraph
updateInterval time.Duration
metrics map[string]prometheus.Gauge
}
type MemoryInfo struct {
Total uint64
Used uint64
UsedPercent float64
func NewMemWidget(updateInterval time.Duration, horizontalScale int) *MemWidget {
self := &MemWidget{
LineGraph: ui.NewLineGraph(),
updateInterval: updateInterval,
}
self.Title = " Memory Usage "
self.HorizontalScale = horizontalScale
mems := make(map[string]devices.MemoryInfo)
devices.UpdateMem(mems)
for name, mem := range mems {
log.Printf("setting %s to %v", name, mem)
self.Data[name] = []float64{0}
self.renderMemInfo(name, mem)
}
go func() {
for range time.NewTicker(self.updateInterval).C {
self.Lock()
devices.UpdateMem(mems)
for label, mi := range mems {
log.Printf(" updating %s to %v", label, mi)
self.renderMemInfo(label, mi)
if self.metrics != nil && self.metrics[label] != nil {
self.metrics[label].Set(mi.UsedPercent)
}
}
self.Unlock()
}
}()
return self
}
func (self *MemWidget) renderMemInfo(line string, memoryInfo MemoryInfo) {
func (b *MemWidget) EnableMetric() {
b.metrics = make(map[string]prometheus.Gauge)
mems := make(map[string]devices.MemoryInfo)
devices.UpdateMem(mems)
for l, mem := range mems {
b.metrics[l] = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "gotop",
Subsystem: "memory",
Name: l,
})
b.metrics[l].Set(mem.UsedPercent)
prometheus.MustRegister(b.metrics[l])
}
}
func (b *MemWidget) Scale(i int) {
b.LineGraph.HorizontalScale = i
}
func (self *MemWidget) renderMemInfo(line string, memoryInfo devices.MemoryInfo) {
self.Data[line] = append(self.Data[line], memoryInfo.UsedPercent)
memoryTotalBytes, memoryTotalMagnitude := utils.ConvertBytes(memoryInfo.Total)
memoryUsedBytes, memoryUsedMagnitude := utils.ConvertBytes(memoryInfo.Used)
@ -34,45 +82,3 @@ func (self *MemWidget) renderMemInfo(line string, memoryInfo MemoryInfo) {
memoryTotalMagnitude,
)
}
func (self *MemWidget) updateMainMemory() {
mainMemory, err := psMem.VirtualMemory()
if err != nil {
log.Printf("failed to get main memory info from gopsutil: %v", err)
} else {
self.renderMemInfo("Main", MemoryInfo{
Total: mainMemory.Total,
Used: mainMemory.Used,
UsedPercent: mainMemory.UsedPercent,
})
}
}
func NewMemWidget(updateInterval time.Duration, horizontalScale int) *MemWidget {
self := &MemWidget{
LineGraph: ui.NewLineGraph(),
updateInterval: updateInterval,
}
self.Title = " Memory Usage "
self.HorizontalScale = horizontalScale
self.Data["Main"] = []float64{0}
self.Data["Swap"] = []float64{0}
self.updateMainMemory()
self.updateSwapMemory()
go func() {
for range time.NewTicker(self.updateInterval).C {
self.Lock()
self.updateMainMemory()
self.updateSwapMemory()
self.Unlock()
}
}()
return self
}
func (b *MemWidget) Scale(i int) {
b.LineGraph.HorizontalScale = i
}

View File

@ -1,61 +0,0 @@
package widgets
import (
"fmt"
"log"
"os/exec"
"strconv"
"strings"
"github.com/xxxserxxx/gotop/utils"
)
func convert(s []string) (MemoryInfo, error) {
total, err := strconv.ParseUint(s[0], 10, 64)
if err != nil {
return MemoryInfo{}, fmt.Errorf("int converion failed %v", err)
}
used, err := strconv.ParseUint(s[1], 10, 64)
if err != nil {
return MemoryInfo{}, fmt.Errorf("int converion failed %v", err)
}
percentage, err := strconv.ParseFloat(strings.TrimSuffix(s[2], "%"), 64)
if err != nil {
return MemoryInfo{}, fmt.Errorf("float converion failed %v", err)
}
return MemoryInfo{
Total: total * utils.KB,
Used: used * utils.KB,
UsedPercent: percentage,
}, nil
}
func gatherSwapInfo() (MemoryInfo, error) {
cmd := "swapinfo -k|sed -n '1!p'|awk '{print $2,$3,$5}'"
output, err := exec.Command("sh", "-c", cmd).Output()
if err != nil {
if err != nil {
return MemoryInfo{}, fmt.Errorf("command failed %v", err)
}
}
ss := strings.Split(strings.TrimSuffix(string(output), "\n"), " ")
return convert(ss)
}
func (self *MemWidget) updateSwapMemory() {
swapMemory, err := gatherSwapInfo()
if err != nil {
log.Printf("failed to get swap memory info from gopsutil: %v", err)
} else {
self.renderMemInfo("Swap", MemoryInfo{
Total: swapMemory.Total,
Used: swapMemory.Used,
UsedPercent: swapMemory.UsedPercent,
})
}
}

View File

@ -1,22 +0,0 @@
// +build !freebsd
package widgets
import (
"log"
psMem "github.com/shirou/gopsutil/mem"
)
func (self *MemWidget) updateSwapMemory() {
swapMemory, err := psMem.SwapMemory()
if err != nil {
log.Printf("failed to get swap memory info from gopsutil: %v", err)
} else {
self.renderMemInfo("Swap", MemoryInfo{
Total: swapMemory.Total,
Used: swapMemory.Used,
UsedPercent: swapMemory.UsedPercent,
})
}
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
psNet "github.com/shirou/gopsutil/net"
ui "github.com/xxxserxxx/gotop/termui"
@ -25,6 +26,8 @@ type NetWidget struct {
totalBytesRecv uint64
totalBytesSent uint64
NetInterface []string
sentMetric prometheus.Counter
recvMetric prometheus.Counter
}
// TODO: state:merge #169 % option for network use (jrswab/networkPercentage)
@ -59,6 +62,22 @@ func NewNetWidget(netInterface string) *NetWidget {
return self
}
func (b *NetWidget) EnableMetric() {
b.recvMetric = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "gotop",
Subsystem: "net",
Name: "recv",
})
prometheus.MustRegister(b.recvMetric)
b.sentMetric = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "gotop",
Subsystem: "net",
Name: "sent",
})
prometheus.MustRegister(b.sentMetric)
}
func (self *NetWidget) update() {
interfaces, err := psNet.IOCounters(true)
if err != nil {
@ -115,6 +134,10 @@ func (self *NetWidget) update() {
self.Lines[0].Data = append(self.Lines[0].Data, int(recentBytesRecv))
self.Lines[1].Data = append(self.Lines[1].Data, int(recentBytesSent))
if self.sentMetric != nil {
self.sentMetric.Add(float64(recentBytesSent))
self.recvMetric.Add(float64(recentBytesRecv))
}
}
// used in later calls to update

View File

@ -100,6 +100,10 @@ func NewProcWidget() *ProcWidget {
return self
}
func (p *ProcWidget) EnableMetric() {
// There's (currently) no metric for this
}
func (self *ProcWidget) SetEditingFilter(editing bool) {
self.entry.SetEditing(editing)
}

View File

@ -7,16 +7,17 @@ import (
"time"
ui "github.com/gizak/termui/v3"
"github.com/prometheus/client_golang/prometheus"
"github.com/xxxserxxx/gotop/devices"
"github.com/xxxserxxx/gotop/utils"
)
type TempScale int
type TempScale rune
const (
Celsius TempScale = 0
Fahrenheit = 1
Disabled = 2
Celsius TempScale = 'C'
Fahrenheit = 'F'
)
type TempWidget struct {
@ -27,6 +28,7 @@ type TempWidget struct {
TempLowColor ui.Color
TempHighColor ui.Color
TempScale TempScale
tempsMetric map[string]prometheus.Gauge
}
// TODO: state:deferred 156 Added temperatures for NVidia GPUs (azak-azkaran/master). Crashes on non-nvidia machines.
@ -57,6 +59,20 @@ func NewTempWidget(tempScale TempScale) *TempWidget {
return self
}
func (self *TempWidget) EnableMetric() {
self.tempsMetric = make(map[string]prometheus.Gauge)
for k, v := range self.Data {
gauge := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "gotop",
Subsystem: "temp",
Name: k,
})
gauge.Set(float64(v))
prometheus.MustRegister(gauge)
self.tempsMetric[k] = gauge
}
}
// Custom Draw method instead of inheriting from a generic Widget.
func (self *TempWidget) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
@ -85,20 +101,26 @@ func (self *TempWidget) Draw(buf *ui.Buffer) {
image.Pt(self.Inner.Min.X, self.Inner.Min.Y+y),
)
// TODO: state:merge #184 or #177 degree symbol (BartWillems/master, fleaz/master)
switch self.TempScale {
case Fahrenheit:
buf.SetString(
fmt.Sprintf("%3dF", self.Data[key]),
ui.NewStyle(fg),
image.Pt(self.Inner.Max.X-4, self.Inner.Min.Y+y),
)
case Celsius:
buf.SetString(
fmt.Sprintf("%3dC", self.Data[key]),
ui.NewStyle(fg),
image.Pt(self.Inner.Max.X-4, self.Inner.Min.Y+y),
)
if self.tempsMetric != nil {
self.tempsMetric[key].Set(float64(self.Data[key]))
}
temperature := fmt.Sprintf("%3d°%c", self.Data[key], self.TempScale)
buf.SetString(
temperature,
ui.NewStyle(fg),
image.Pt(self.Inner.Max.X-(len(temperature)-1), self.Inner.Min.Y+y),
)
}
}
func (self *TempWidget) update() {
devices.UpdateTemps(self.Data)
for name, val := range self.Data {
if self.TempScale == Fahrenheit {
self.Data[name] = utils.CelsiusToFahrenheit(val)
} else {
self.Data[name] = val
}
}
}

View File

@ -1,73 +0,0 @@
// +build freebsd
package widgets
import (
"fmt"
"log"
"os/exec"
"strconv"
"strings"
"github.com/xxxserxxx/gotop/utils"
)
var sensorOIDS = map[string]string{
"dev.cpu.0.temperature": "CPU 0 ",
"hw.acpi.thermal.tz0.temperature": "Thermal zone 0",
}
type sensorMeasurement struct {
name string
temperature float64
}
func removeUnusedChars(s string) string {
s1 := strings.Replace(s, "C", "", 1)
s2 := strings.TrimSuffix(s1, "\n")
return s2
}
func refineOutput(output []byte) (float64, error) {
convertedOutput := utils.ConvertLocalizedString(removeUnusedChars(string(output)))
value, err := strconv.ParseFloat(convertedOutput, 64)
if err != nil {
return 0, err
}
return value, nil
}
func collectSensors() ([]sensorMeasurement, error) {
var measurements []sensorMeasurement
for k, v := range sensorOIDS {
output, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
return nil, fmt.Errorf("failed to execute 'sysctl' command: %v", err)
}
value, err := refineOutput(output)
if err != nil {
return nil, fmt.Errorf("failed to execute 'sysctl' command: %v", err)
}
measurements = append(measurements, sensorMeasurement{v, value})
}
return measurements, nil
}
func (self *TempWidget) update() {
sensors, err := collectSensors()
if err != nil {
log.Printf("error received from gopsutil: %v", err)
}
for _, sensor := range sensors {
switch self.TempScale {
case Fahrenheit:
self.Data[sensor.name] = utils.CelsiusToFahrenheit(int(sensor.temperature))
case Celsius:
self.Data[sensor.name] = int(sensor.temperature)
}
}
}

View File

@ -1,29 +0,0 @@
// +build windows
package widgets
import (
"log"
psHost "github.com/shirou/gopsutil/host"
"github.com/xxxserxxx/gotop/utils"
)
func (self *TempWidget) update() {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
log.Printf("failed to get sensors from gopsutil: %v", err)
return
}
for _, sensor := range sensors {
if sensor.Temperature != 0 {
switch self.TempScale {
case Fahrenheit:
self.Data[sensor.SensorKey] = utils.CelsiusToFahrenheit(int(sensor.Temperature))
case Celsius:
self.Data[sensor.SensorKey] = int(sensor.Temperature)
}
}
}
}