diff --git a/CHANGELOG.md b/CHANGELOG.md index 8644dd6..2f96d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go index 5029519..0fae9e2 100644 --- a/cmd/gotop/main.go +++ b/cmd/gotop/main.go @@ -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) + } +} diff --git a/colorschemes/default.go b/colorschemes/default.go index dc6c6b2..90ac3fa 100644 --- a/colorschemes/default.go +++ b/colorschemes/default.go @@ -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, diff --git a/colorschemes/default.json b/colorschemes/default.json index 12cab1b..d6b3dc5 100644 --- a/colorschemes/default.json +++ b/colorschemes/default.json @@ -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, diff --git a/colorschemes/default_dark.go b/colorschemes/default_dark.go index 849f85b..7f7911c 100644 --- a/colorschemes/default_dark.go +++ b/colorschemes/default_dark.go @@ -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, diff --git a/colorschemes/monokai.go b/colorschemes/monokai.go index cd0471c..d09f3c9 100644 --- a/colorschemes/monokai.go +++ b/colorschemes/monokai.go @@ -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, diff --git a/colorschemes/nord.go b/colorschemes/nord.go index b6d4b50..028057e 100644 --- a/colorschemes/nord.go +++ b/colorschemes/nord.go @@ -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) diff --git a/colorschemes/solarized.go b/colorschemes/solarized.go index 48d67b0..4649d93 100644 --- a/colorschemes/solarized.go +++ b/colorschemes/solarized.go @@ -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, diff --git a/colorschemes/solarized16_dark.go b/colorschemes/solarized16_dark.go index 354991b..00c7eb6 100644 --- a/colorschemes/solarized16_dark.go +++ b/colorschemes/solarized16_dark.go @@ -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, diff --git a/colorschemes/solarized16_light.go b/colorschemes/solarized16_light.go index 0fae397..b4157d9 100644 --- a/colorschemes/solarized16_light.go +++ b/colorschemes/solarized16_light.go @@ -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, diff --git a/colorschemes/template.go b/colorschemes/template.go index 9b7ac79..af4b792 100644 --- a/colorschemes/template.go +++ b/colorschemes/template.go @@ -32,8 +32,7 @@ type Colorscheme struct { BattLines []int - MainMem int - SwapMem int + MemLines []int ProcCursor int diff --git a/colorschemes/vice.go b/colorschemes/vice.go index 8bd2545..d0dbb03 100644 --- a/colorschemes/vice.go +++ b/colorschemes/vice.go @@ -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, diff --git a/config.go b/config.go index 7fe2d72..9109a4b 100644 --- a/config.go +++ b/config.go @@ -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], ",") } } diff --git a/devices/cpu.go b/devices/cpu.go new file mode 100644 index 0000000..436ccb1 --- /dev/null +++ b/devices/cpu.go @@ -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) + } + } + } +} diff --git a/devices/cpu_cpu.go b/devices/cpu_cpu.go new file mode 100644 index 0000000..a1d20cd --- /dev/null +++ b/devices/cpu_cpu.go @@ -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) +} diff --git a/devices/devices.go b/devices/devices.go new file mode 100644 index 0000000..91a8815 --- /dev/null +++ b/devices/devices.go @@ -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) + } + } +} diff --git a/widgets/include/smc.c b/devices/include/smc.c similarity index 100% rename from widgets/include/smc.c rename to devices/include/smc.c diff --git a/widgets/include/smc.h b/devices/include/smc.h similarity index 100% rename from widgets/include/smc.h rename to devices/include/smc.h diff --git a/devices/mem.go b/devices/mem.go new file mode 100644 index 0000000..defb01f --- /dev/null +++ b/devices/mem.go @@ -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) + } + } + } +} diff --git a/devices/mem_mem.go b/devices/mem_mem.go new file mode 100644 index 0000000..53e5721 --- /dev/null +++ b/devices/mem_mem.go @@ -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) +} diff --git a/devices/mem_swap_freebsd.go b/devices/mem_swap_freebsd.go new file mode 100644 index 0000000..3a95aa9 --- /dev/null +++ b/devices/mem_swap_freebsd.go @@ -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) +} diff --git a/devices/mem_swap_other.go b/devices/mem_swap_other.go new file mode 100644 index 0000000..fb16705 --- /dev/null +++ b/devices/mem_swap_other.go @@ -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) +} diff --git a/devices/temp.go b/devices/temp.go new file mode 100644 index 0000000..010e7ad --- /dev/null +++ b/devices/temp.go @@ -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) + } + } + } +} diff --git a/widgets/temp_darwin.go b/devices/temp_darwin.go similarity index 57% rename from widgets/temp_darwin.go rename to devices/temp_darwin.go index d0e512b..e60b4be 100644 --- a/widgets/temp_darwin.go +++ b/devices/temp_darwin.go @@ -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 } diff --git a/devices/temp_freebsd.go b/devices/temp_freebsd.go new file mode 100644 index 0000000..ba47474 --- /dev/null +++ b/devices/temp_freebsd.go @@ -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 +} diff --git a/widgets/temp_linux.go b/devices/temp_linux.go similarity index 58% rename from widgets/temp_linux.go rename to devices/temp_linux.go index 1460007..ec4ff95 100644 --- a/widgets/temp_linux.go +++ b/devices/temp_linux.go @@ -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 } diff --git a/widgets/temp_openbsd.go b/devices/temp_openbsd.go similarity index 73% rename from widgets/temp_openbsd.go rename to devices/temp_openbsd.go index 0d4a24a..410cc4a 100644 --- a/widgets/temp_openbsd.go +++ b/devices/temp_openbsd.go @@ -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 + } } } diff --git a/devices/temp_windows.go b/devices/temp_windows.go new file mode 100644 index 0000000..230e902 --- /dev/null +++ b/devices/temp_windows.go @@ -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 +} diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 0000000..b88f63f --- /dev/null +++ b/docs/extensions.md @@ -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. diff --git a/docs/grid-fill.md b/docs/grid-fill.md new file mode 100644 index 0000000..554b524 --- /dev/null +++ b/docs/grid-fill.md @@ -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 diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..540c950 --- /dev/null +++ b/docs/releasing.md @@ -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 diff --git a/go.mod b/go.mod index 639a0d1..1e4a5bb 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index aaabcf8..3cf7498 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/layout/layout.go b/layout/layout.go index b8d3226..d7fbc22 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -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 } 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..71a542f --- /dev/null +++ b/layouts/kitchensink @@ -0,0 +1,4 @@ +3:cpu/2 3:mem/1 +4:temp/1 3:disk/2 + power +3:net 3: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/make.sh b/make.sh index 961e282..46ecbce 100755 --- a/make.sh +++ b/make.sh @@ -92,6 +92,7 @@ function cdarwinz() { cd darwin else cd darwin + git checkout -- . git pull fi export CGO_ENABLED=1 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..59accfb --- /dev/null +++ b/widgets/batterygauge.go @@ -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) + } +} diff --git a/widgets/cpu.go b/widgets/cpu.go index 8e8819c..53f5529 100644 --- a/widgets/cpu.go +++ b/widgets/cpu.go @@ -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)) } } } 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..b80b68c 100644 --- a/widgets/mem.go +++ b/widgets/mem.go @@ -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 -} diff --git a/widgets/mem_freebsd.go b/widgets/mem_freebsd.go deleted file mode 100644 index 0d5b0b6..0000000 --- a/widgets/mem_freebsd.go +++ /dev/null @@ -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, - }) - } -} diff --git a/widgets/mem_other.go b/widgets/mem_other.go deleted file mode 100644 index 6b78acb..0000000 --- a/widgets/mem_other.go +++ /dev/null @@ -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, - }) - } -} diff --git a/widgets/net.go b/widgets/net.go index ba2660d..1326fdc 100644 --- a/widgets/net.go +++ b/widgets/net.go @@ -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 diff --git a/widgets/proc.go b/widgets/proc.go index 9fed067..e4a8ace 100644 --- a/widgets/proc.go +++ b/widgets/proc.go @@ -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) } diff --git a/widgets/temp.go b/widgets/temp.go index 7c0a358..a01853e 100644 --- a/widgets/temp.go +++ b/widgets/temp.go @@ -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 } } } diff --git a/widgets/temp_freebsd.go b/widgets/temp_freebsd.go deleted file mode 100644 index 8d9eda6..0000000 --- a/widgets/temp_freebsd.go +++ /dev/null @@ -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) - } - } -} diff --git a/widgets/temp_windows.go b/widgets/temp_windows.go deleted file mode 100644 index 422e395..0000000 --- a/widgets/temp_windows.go +++ /dev/null @@ -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) - } - } - } -}