xmtop/cmd/gotop/main.go
Clayton Townsend II 9238e81a59 add page up and page down to for scrolling pages
adds page up and page down keys to main.go to scroll page up and scroll page down
similar to behavior found in htop, and slightly more intuitive
2022-07-22 15:21:47 -05:00

555 lines
14 KiB
Go

package main
import (
"bufio"
_ "embed"
"flag"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
//_ "net/http/pprof"
"github.com/VictoriaMetrics/metrics"
jj "github.com/cloudfoundry-attic/jibber_jabber"
"github.com/droundy/goopt"
ui "github.com/gizak/termui/v3"
"github.com/shibukawa/configdir"
"github.com/xxxserxxx/lingo/v2"
"github.com/xxxserxxx/gotop/v4"
"github.com/xxxserxxx/gotop/v4/colorschemes"
"github.com/xxxserxxx/gotop/v4/devices"
"github.com/xxxserxxx/gotop/v4/layout"
"github.com/xxxserxxx/gotop/v4/logging"
w "github.com/xxxserxxx/gotop/v4/widgets"
)
const (
graphHorizontalScaleDelta = 3
defaultUI = "2:cpu\ndisk/1 2:mem/2\ntemp\n2:net 2: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"
kitchensink = "3:cpu/2 3:mem/1\n4:temp/1 3:disk/2\npower\n3:net 3:procs"
)
var (
// Version of the program; set during build from git tags
Version = "0.0.0"
// BuildDate when the program was compiled; set during build
BuildDate = "Hadean"
conf gotop.Config
help *w.HelpMenu
bar *w.StatusBar
stderrLogger = log.New(os.Stderr, "", 0)
tr lingo.Translations
)
//go:embed "description.txt"
var description string
func parseArgs() error {
cds := conf.ConfigDir.QueryFolders(configdir.All)
cpaths := make([]string, len(cds))
for i, p := range cds {
cpaths[i] = p.Path
}
goopt.Summary = "A terminal based graphical activity monitor, inspired by gtop and vtop"
goopt.Version = Version
goopt.Description = func() string {
return description
}
color := goopt.String([]string{"--color", "-c"}, conf.Colorscheme.Name, tr.Value("args.color"))
graphhorizontalscale := goopt.Int([]string{"--graphscale", "-S"}, conf.GraphHorizontalScale, tr.Value("args.scale"))
version := goopt.Flag([]string{"-v", "-V", "--version"}, []string{}, tr.Value("args.version"), "")
percpuload := goopt.Flag([]string{"--percpu", "-p"}, []string{"--no-percpu"}, tr.Value("args.percpu"), tr.Value("args.no-percpu"))
averageload := goopt.Flag([]string{"--averagecpu", "-a"}, []string{"--no-averagecpu"}, tr.Value("args.cpuavg"), tr.Value("args.no-cpuavg"))
tempScale := goopt.Flag([]string{"--fahrenheit"}, []string{"--celsius"}, tr.Value("args.temp"), tr.Value("args.tempc"))
statusbar := goopt.Flag([]string{"--statusbar", "-s"}, []string{"--no-statusbar"}, tr.Value("args.statusbar"), tr.Value("args.no-statusbar"))
updateinterval := goopt.String([]string{"--rate", "-r"}, conf.UpdateInterval.String(), tr.Value("args.rate"))
layout := goopt.String([]string{"--layout", "-l"}, conf.Layout, tr.Value("args.layout"))
netinterface := goopt.String([]string{"--interface", "-i"}, "all", tr.Value("args.net"))
exportport := goopt.String([]string{"--export", "-x"}, conf.ExportPort, tr.Value("args.export"))
mbps := goopt.Flag([]string{"--mbps"}, []string{"--bytes"}, tr.Value("args.mbps"), tr.Value("args.no-mbps"))
test := goopt.Flag([]string{"--test"}, []string{"--no-test"}, tr.Value("args.test"), tr.Value("args.no-test"))
// This is so the flag package doesn't barf on an unrecognized flag; it's processed earlier
goopt.String([]string{"-C"}, "", tr.Value("args.conffile"))
nvidia := goopt.Flag([]string{"--nvidia"}, []string{"--no-nvidia"}, tr.Value("args.nvidia"), tr.Value("args.no-nvidia"))
list := goopt.String([]string{"--list"}, "", tr.Value("args.list"))
wc := goopt.Flag([]string{"--write-config"}, []string{}, tr.Value("args.write"), "")
goopt.Parse(nil)
conf.PercpuLoad = *percpuload
conf.GraphHorizontalScale = *graphhorizontalscale
conf.PercpuLoad = *percpuload
conf.AverageLoad = *averageload
conf.Statusbar = *statusbar
conf.Layout = *layout
conf.NetInterface = *netinterface
conf.ExportPort = *exportport
conf.Mbps = *mbps
conf.Nvidia = *nvidia
conf.AverageLoad = *averageload
conf.Test = *test
conf.Statusbar = *statusbar
conf.Mbps = *mbps
conf.Nvidia = *nvidia
if upInt, err := time.ParseDuration(*updateinterval); err == nil {
conf.UpdateInterval = upInt
} else {
fmt.Printf("Update interval must be a time interval such as '10s' or '1m'")
os.Exit(1)
}
if *tempScale {
conf.TempScale = 'F'
} else {
conf.TempScale = 'C'
}
if *version {
fmt.Printf("gotop %s (%s)\n", Version, BuildDate)
os.Exit(0)
}
if *color != "" {
cs, err := colorschemes.FromName(conf.ConfigDir, *color)
if err != nil {
return err
}
conf.Colorscheme = cs
}
if *list != "" {
switch *list {
case "layouts":
fmt.Println(tr.Value("help.layouts"))
case "colorschemes":
fmt.Println(tr.Value("help.colorschemes"))
case "paths":
fmt.Println(tr.Value("help.paths"))
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.All) {
paths = append(paths, d.Path)
}
fmt.Println(strings.Join(paths, "\n"))
fmt.Println()
fmt.Println(tr.Value("help.log", filepath.Join(conf.ConfigDir.QueryCacheFolder().Path, logging.LOGFILE)))
case "devices":
listDevices()
case "keys":
fmt.Println(tr.Value("help.help"))
case "widgets":
fmt.Println(tr.Value("help.widgets"))
case "langs":
err := fs.WalkDir(gotop.Dicts, ".", func(pth string, info fs.DirEntry, err error) error {
if err != nil {
return err
}
if info.IsDir() { // We skip these
return nil
}
fileName := info.Name()
if strings.HasSuffix(fileName, ".toml") {
fmt.Println(strings.TrimSuffix(fileName, ".toml"))
}
return nil
})
if err != nil {
return err
}
default:
fmt.Printf(tr.Value("error.unknownopt", *list))
os.Exit(1)
}
os.Exit(0)
}
if conf.Nvidia {
conf.ExtensionVars["nvidia"] = "true"
}
if *wc {
path, err := conf.Write()
if err != nil {
fmt.Println(tr.Value("error.writefail", err.Error()))
os.Exit(1)
}
fmt.Println(tr.Value("help.written", path))
os.Exit(0)
}
return nil
}
func setDefaultTermuiColors(c gotop.Config) {
ui.Theme.Default = ui.NewStyle(ui.Color(c.Colorscheme.Fg), ui.Color(c.Colorscheme.Bg))
ui.Theme.Block.Title = ui.NewStyle(ui.Color(c.Colorscheme.BorderLabel), ui.Color(c.Colorscheme.Bg))
ui.Theme.Block.Border = ui.NewStyle(ui.Color(c.Colorscheme.BorderLine), ui.Color(c.Colorscheme.Bg))
}
func eventLoop(c gotop.Config, grid *layout.MyGrid) {
drawTicker := time.NewTicker(c.UpdateInterval).C
// handles kill signal sent to gotop
sigTerm := make(chan os.Signal, 2)
signal.Notify(sigTerm, os.Interrupt, syscall.SIGTERM)
uiEvents := ui.PollEvents()
previousKey := ""
for {
select {
case <-sigTerm:
return
case <-drawTicker:
if !c.HelpVisible {
ui.Render(grid)
if c.Statusbar {
ui.Render(bar)
}
}
case e := <-uiEvents:
if grid.Proc != nil && grid.Proc.HandleEvent(e) {
ui.Render(grid.Proc)
break
}
switch e.ID {
case "q", "<C-c>":
return
case "?":
c.HelpVisible = !c.HelpVisible
case "<Resize>":
payload := e.Payload.(ui.Resize)
termWidth, termHeight := payload.Width, payload.Height
if c.Statusbar {
grid.SetRect(0, 0, termWidth, termHeight-1)
bar.SetRect(0, termHeight-1, termWidth, termHeight)
} else {
grid.SetRect(0, 0, payload.Width, payload.Height)
}
help.Resize(payload.Width, payload.Height)
ui.Clear()
}
if c.HelpVisible {
switch e.ID {
case "?":
ui.Clear()
ui.Render(help)
case "<Escape>":
c.HelpVisible = false
ui.Render(grid)
case "<Resize>":
ui.Render(help)
}
} else {
switch e.ID {
case "?":
ui.Render(grid)
case "h":
c.GraphHorizontalScale += graphHorizontalScaleDelta
for _, item := range grid.Lines {
item.Scale(c.GraphHorizontalScale)
}
ui.Render(grid)
case "l":
if c.GraphHorizontalScale > graphHorizontalScaleDelta {
c.GraphHorizontalScale -= graphHorizontalScaleDelta
for _, item := range grid.Lines {
item.Scale(c.GraphHorizontalScale)
ui.Render(item)
}
}
case "b":
if grid.Net != nil {
grid.Net.Mbps = !grid.Net.Mbps
}
case "<Resize>":
ui.Render(grid)
if c.Statusbar {
ui.Render(bar)
}
case "<MouseLeft>":
if grid.Proc != nil {
payload := e.Payload.(ui.Mouse)
grid.Proc.HandleClick(payload.X, payload.Y)
ui.Render(grid.Proc)
}
case "k", "<Up>", "<MouseWheelUp>":
if grid.Proc != nil {
grid.Proc.ScrollUp()
ui.Render(grid.Proc)
}
case "j", "<Down>", "<MouseWheelDown>":
if grid.Proc != nil {
grid.Proc.ScrollDown()
ui.Render(grid.Proc)
}
case "<Home>":
if grid.Proc != nil {
grid.Proc.ScrollTop()
ui.Render(grid.Proc)
}
case "g":
if grid.Proc != nil {
if previousKey == "g" {
grid.Proc.ScrollTop()
ui.Render(grid.Proc)
}
}
case "G", "<End>":
if grid.Proc != nil {
grid.Proc.ScrollBottom()
ui.Render(grid.Proc)
}
case "<C-d>":
if grid.Proc != nil {
grid.Proc.ScrollHalfPageDown()
ui.Render(grid.Proc)
}
case "<C-u>":
if grid.Proc != nil {
grid.Proc.ScrollHalfPageUp()
ui.Render(grid.Proc)
}
case "<C-f>", "<PageDown>":
if grid.Proc != nil {
grid.Proc.ScrollPageDown()
ui.Render(grid.Proc)
}
case "<C-b>", "<PageUp>":
if grid.Proc != nil {
grid.Proc.ScrollPageUp()
ui.Render(grid.Proc)
}
case "d":
if grid.Proc != nil {
if previousKey == "d" {
grid.Proc.KillProc("SIGTERM")
}
}
case "3":
if grid.Proc != nil {
if previousKey == "d" {
grid.Proc.KillProc("SIGQUIT")
}
}
case "9":
if grid.Proc != nil {
if previousKey == "d" {
grid.Proc.KillProc("SIGKILL")
}
}
case "<Tab>":
if grid.Proc != nil {
grid.Proc.ToggleShowingGroupedProcs()
ui.Render(grid.Proc)
}
case "m", "c", "p":
if grid.Proc != nil {
grid.Proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID))
ui.Render(grid.Proc)
}
case "/":
if grid.Proc != nil {
grid.Proc.SetEditingFilter(true)
ui.Render(grid.Proc)
}
}
if previousKey == e.ID {
previousKey = ""
} else {
previousKey = e.ID
}
}
}
}
}
// FIXME CPU use regression
// TODO add CPU freq
func main() {
// TODO: Make this an option, for performance testing
//go func() {
// log.Fatal(http.ListenAndServe(":7777", nil))
//}()
// This is just to make sure gotop returns a useful exit code, but also
// executes all defer statements and so cleans up before exit. Sort of
// annoying work-around for a lack of a clean way to exit Go programs
// with exit codes.
ec := run()
if ec > 0 {
if ec < 2 {
logpath := filepath.Join(conf.ConfigDir.QueryCacheFolder().Path, logging.LOGFILE)
fmt.Println(tr.Value("error.checklog", logpath))
bs, _ := ioutil.ReadFile(logpath)
fmt.Println(string(bs))
}
}
os.Exit(ec)
}
func run() int {
ling, err := lingo.New("en_US", ".", gotop.Dicts)
if err != nil {
fmt.Printf("failed to load language files: %s\n", err)
return 2
}
lang, err := jj.DetectIETF()
if err != nil {
lang = "en_US"
}
lang = strings.Replace(lang, "-", "_", -1)
// Get the locale from the os
tr = ling.TranslationsForLocale(lang)
colorschemes.SetTr(tr)
conf = gotop.NewConfig()
conf.Tr = tr
// Find the config file; look in (1) local, (2) user, (3) global
// Check the last argument first
fs := flag.NewFlagSet("config", flag.ContinueOnError)
cfg := fs.String("C", "", tr.Value("configfile"))
fs.SetOutput(bufio.NewWriter(nil))
fs.Parse(os.Args[1:])
if *cfg != "" {
conf.ConfigFile = *cfg
}
err = conf.Load()
if err != nil {
fmt.Println(tr.Value("error.configparse", err.Error()))
return 2
}
// Override with command line arguments
err = parseArgs()
if err != nil {
fmt.Println(tr.Value("error.cliparse", err.Error()))
return 2
}
logfile, err := logging.New(conf)
if err != nil {
fmt.Println(tr.Value("logsetup", err.Error()))
return 2
}
defer logfile.Close()
// device initialization errors do not stop execution
for _, err := range devices.Startup(conf.ExtensionVars) {
stderrLogger.Print(err)
}
lstream, err := getLayout(conf)
if err != nil {
stderrLogger.Print(err)
return 1
}
ly := layout.ParseLayout(lstream)
if conf.Test {
return runTests(conf)
}
if err = ui.Init(); err != nil {
stderrLogger.Print(err)
return 1
}
defer ui.Close()
setDefaultTermuiColors(conf) // done before initializing widgets to allow inheriting colors
help = w.NewHelpMenu(tr)
if conf.Statusbar {
bar = w.NewStatusBar()
}
grid, err := layout.Layout(ly, conf)
if err != nil {
stderrLogger.Print(err)
return 1
}
termWidth, termHeight := ui.TerminalDimensions()
if conf.Statusbar {
grid.SetRect(0, 0, termWidth, termHeight-1)
} else {
grid.SetRect(0, 0, termWidth, termHeight)
}
help.Resize(termWidth, termHeight)
ui.Render(grid)
if conf.Statusbar {
bar.SetRect(0, termHeight-1, termWidth, termHeight)
ui.Render(bar)
}
// TODO https://godoc.org/github.com/VictoriaMetrics/metrics#Set
if conf.ExportPort != "" {
go func() {
http.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) {
metrics.WritePrometheus(w, true)
})
http.ListenAndServe(conf.ExportPort, nil)
}()
}
eventLoop(conf, grid)
return 0
}
func getLayout(conf gotop.Config) (io.Reader, error) {
switch conf.Layout {
case "-":
return os.Stdin, nil
case "default":
return strings.NewReader(defaultUI), nil
case "minimal":
return strings.NewReader(minimalUI), nil
case "battery":
return strings.NewReader(batteryUI), nil
case "procs":
return strings.NewReader(procsUI), nil
case "kitchensink":
return strings.NewReader(kitchensink), nil
default:
folder := conf.ConfigDir.QueryFolderContainsFile(conf.Layout)
if folder == nil {
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.Existing) {
paths = append(paths, d.Path)
}
return nil, fmt.Errorf(tr.Value("error.findlayout", conf.Layout, strings.Join(paths, ", ")))
}
lo, err := folder.ReadFile(conf.Layout)
if err != nil {
return nil, err
}
return strings.NewReader(string(lo)), nil
}
}
func runTests(_ gotop.Config) int {
fmt.Printf("PASS")
return 0
}
func listDevices() {
ms := devices.Domains
sort.Strings(ms)
for _, m := range ms {
fmt.Printf("%s:\n", m)
for _, d := range devices.Devices(m, true) {
fmt.Printf("\t%s\n", d)
}
}
}