diff --git a/CHANGELOG.md b/CHANGELOG.md index b40b415..3f6ac8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds ability to filter reported temperatures (#92) - Command line option to list layouts, paths, colorschemes, hotkeys, and filterable devices - Adds ability to write out a configuration file +- Adds a command for specifying the configuration file to use ### Changed diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go index 1cbc50d..25dd5b8 100644 --- a/cmd/gotop/main.go +++ b/cmd/gotop/main.go @@ -1,6 +1,8 @@ package main import ( + "bufio" + "flag" "fmt" "io" "log" @@ -18,7 +20,7 @@ import ( ui "github.com/gizak/termui/v3" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/shibukawa/configdir" - flag "github.com/xxxserxxx/opflag" + "github.com/xxxserxxx/opflag" "github.com/xxxserxxx/gotop/v4" "github.com/xxxserxxx/gotop/v4/colorschemes" @@ -50,49 +52,50 @@ var ( stderrLogger = log.New(os.Stderr, "", 0) ) -func parseArgs(conf *gotop.Config) error { +func parseArgs() error { cds := conf.ConfigDir.QueryFolders(configdir.All) cpaths := make([]string, len(cds)) for i, p := range cds { cpaths[i] = p.Path } - help := flag.BoolP("help", "h", false, "Show this screen.") - color := flag.StringP("color", "c", conf.Colorscheme.Name, "Set a colorscheme.") - flag.IntVarP(&conf.GraphHorizontalScale, "graphscale", "S", conf.GraphHorizontalScale, "Graph scale factor, >0") - version := flag.BoolP("version", "v", false, "Print version and exit.") - versioN := flag.BoolP("", "V", false, "Print version and exit.") - flag.BoolVarP(&conf.PercpuLoad, "percpu", "p", conf.PercpuLoad, "Show each CPU in the CPU widget.") - flag.BoolVarP(&conf.AverageLoad, "averagecpu", "a", conf.AverageLoad, "Show average CPU in the CPU widget.") - fahrenheit := flag.BoolP("fahrenheit", "f", conf.TempScale == 'F', "Show temperatures in fahrenheit.Show temperatures in fahrenheit.") - flag.BoolVarP(&conf.Statusbar, "statusbar", "s", conf.Statusbar, "Show a statusbar with the time.") - flag.DurationVarP(&conf.UpdateInterval, "rate", "r", conf.UpdateInterval, "Number of times per second to update CPU and Mem widgets.") - flag.StringVarP(&conf.Layout, "layout", "l", conf.Layout, `Name of layout spec file for the UI. Use "-" to pipe.`) - flag.StringVarP(&conf.NetInterface, "interface", "i", "all", "Select network interface. Several interfaces can be defined using comma separated values. Interfaces can also be ignored using `!`") - flag.StringVarP(&conf.ExportPort, "export", "x", conf.ExportPort, "Enable metrics for export on the specified port.") - flag.BoolVarP(&conf.Mbps, "mbps", "", conf.Mbps, "Show network rate as mbps.") + help := opflag.BoolP("help", "h", false, "Show this screen.") + color := opflag.StringP("color", "c", conf.Colorscheme.Name, "Set a colorscheme.") + opflag.IntVarP(&conf.GraphHorizontalScale, "graphscale", "S", conf.GraphHorizontalScale, "Graph scale factor, >0") + version := opflag.BoolP("version", "v", false, "Print version and exit.") + versioN := opflag.BoolP("", "V", false, "Print version and exit.") + opflag.BoolVarP(&conf.PercpuLoad, "percpu", "p", conf.PercpuLoad, "Show each CPU in the CPU widget.") + opflag.BoolVarP(&conf.AverageLoad, "averagecpu", "a", conf.AverageLoad, "Show average CPU in the CPU widget.") + fahrenheit := opflag.BoolP("fahrenheit", "f", conf.TempScale == 'F', "Show temperatures in fahrenheit.Show temperatures in fahrenheit.") + opflag.BoolVarP(&conf.Statusbar, "statusbar", "s", conf.Statusbar, "Show a statusbar with the time.") + opflag.DurationVarP(&conf.UpdateInterval, "rate", "r", conf.UpdateInterval, "Number of times per second to update CPU and Mem widgets.") + opflag.StringVarP(&conf.Layout, "layout", "l", conf.Layout, `Name of layout spec file for the UI. Use "-" to pipe.`) + opflag.StringVarP(&conf.NetInterface, "interface", "i", "all", "Select network interface. Several interfaces can be defined using comma separated values. Interfaces can also be ignored using `!`") + opflag.StringVarP(&conf.ExportPort, "export", "x", conf.ExportPort, "Enable metrics for export on the specified port.") + opflag.BoolVarP(&conf.Mbps, "mbps", "", conf.Mbps, "Show network rate as mbps.") // FIXME Where did this go?? - //conf.Band = flag.IntP("bandwidth", "B", 100, "Specify the number of bits per seconds.") - flag.BoolVar(&conf.Test, "test", conf.Test, "Runs tests and exits with success/failure code.") - list := flag.String("list", "", `List + //conf.Band = opflag.IntP("bandwidth", "B", 100, "Specify the number of bits per seconds.") + opflag.BoolVar(&conf.Test, "test", conf.Test, "Runs tests and exits with success/failure code.") + opflag.StringP("", "C", "", "Config file to use instead of default (MUST BE FIRST ARGUMENT)") + list := opflag.String("list", "", `List devices: Prints out device names for filterable widgets layouts: Lists build-in layouts colorschemes: Lists built-in colorschemes paths: List out configuration file search paths - widgets: Widgets that can be used in a layout + widgets: Widgets that can be used in a layout keys: Show the keyboard bindings.`) - wc := flag.Bool("write-config", false, "Write out a default config file.") - flag.SortFlags = false - flag.Usage = func() { + wc := opflag.Bool("write-config", false, "Write out a default config file.") + opflag.SortFlags = false + opflag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\nOptions:\n", os.Args[0]) - flag.PrintDefaults() + opflag.PrintDefaults() } - flag.Parse() + opflag.Parse() if *version || *versioN { fmt.Printf("gotop %s (%s)\n", Version, BuildDate) os.Exit(0) } if *help { - flag.Usage() + opflag.Usage() os.Exit(0) } cs, err := colorschemes.FromName(conf.ConfigDir, *color) @@ -331,14 +334,17 @@ func eventLoop(c gotop.Config, grid *layout.MyGrid) { } } -// TODO: Add fans -// TODO: mpd visualizer widget +// TODO: @devices fans +// TODO: @devices mpd visualizer +// TODO: @devices color bars for memory, a-la bashtop // TODO: Add tab completion for Linux https://gist.github.com/icholy/5314423 // TODO: state:merge #135 linux console font (cmatsuoka/console-font) // TODO: Abstract out the UI toolkit. mum4k/termdash, VladimirMarkelov/clui, gcla/gowid, rivo/tview, marcusolsson/tui-go might work better for some OS/Archs. Performance/memory use comparison would be interesting. -// TODO: all of the go vet stuff, more unit tests, benchmarks, finish remote. -// TODO: color bars for memory, a-la bashtop +// TODO: more unit tests, benchmarks +// TODO: README is getting long. Move to wiki. // TODO: add verbose debugging option +// TODO: find VMs for FreeBSD, etc for testing gotop +// TODO: add README about extensions, and wiki page for writing extensions func main() { // For performance testing //go func() { @@ -359,15 +365,23 @@ func main() { } func run() int { - conf := gotop.NewConfig() + conf = gotop.NewConfig() // 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", "", "Config file") + fs.SetOutput(bufio.NewWriter(nil)) + fs.Parse(os.Args[1:]) + if *cfg != "" { + conf.ConfigFile = *cfg + } err := conf.Load() if err != nil { fmt.Printf("failed to parse config file: %s\n", err) return 2 } // Override with command line arguments - err = parseArgs(&conf) + err = parseArgs() if err != nil { fmt.Printf("parsing CLI args: %s\n", err) return 2 diff --git a/config.go b/config.go index 6a109e0..0cc5e61 100644 --- a/config.go +++ b/config.go @@ -5,7 +5,9 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "log" + "os" "path/filepath" "strconv" "strings" @@ -16,6 +18,9 @@ import ( "github.com/xxxserxxx/gotop/v4/widgets" ) +// CONFFILE is the name of the default config file +const CONFFILE = "gotop.conf" + type Config struct { ConfigDir configdir.ConfigDir GraphHorizontalScale int @@ -34,6 +39,7 @@ type Config struct { Temps []string Test bool ExtensionVars map[string]string + ConfigFile string } func NewConfig() Config { @@ -54,21 +60,25 @@ func NewConfig() Config { ExtensionVars: make(map[string]string), } conf.Colorscheme, _ = colorschemes.FromName(conf.ConfigDir, "default") + folder := conf.ConfigDir.QueryFolderContainsFile(CONFFILE) + if folder != nil { + conf.ConfigFile = filepath.Join(folder.Path, CONFFILE) + } return conf } func (conf *Config) Load() error { var in []byte - var err error - cfn := "gotop.conf" - folder := conf.ConfigDir.QueryFolderContainsFile(cfn) - if folder != nil { - if in, err = folder.ReadFile(cfn); err != nil { - return err - } - } else { + if conf.ConfigFile == "" { return nil } + var err error + if _, err = os.Stat(conf.ConfigFile); os.IsNotExist(err) { + return nil + } + if in, err = ioutil.ReadFile(conf.ConfigFile); err != nil { + return err + } return load(bytes.NewReader(in), conf) } @@ -169,21 +179,35 @@ func load(in io.Reader, conf *Config) error { return nil } +// Write serializes the configuration to a file. +// The configuration written is based on the loaded configuration, plus any +// command-line changes, so it can be used to update an existing configuration +// file. The file will be written to the specificed `--config` argument file, +// if one is set; otherwise, it'll create one in the user's config directory. func (conf *Config) Write() (string, error) { - cfn := "gotop.conf" - ds := conf.ConfigDir.QueryFolders(configdir.Global) - if len(ds) == 0 { - ds = conf.ConfigDir.QueryFolders(configdir.Local) + var dir *configdir.Config + var file string = CONFFILE + if conf.ConfigFile == "" { + ds := conf.ConfigDir.QueryFolders(configdir.Global) if len(ds) == 0 { - return "", fmt.Errorf("error locating config folders") + ds = conf.ConfigDir.QueryFolders(configdir.Local) + if len(ds) == 0 { + return "", fmt.Errorf("error locating config folders") + } } + ds[0].CreateParentDir(CONFFILE) + dir = ds[0] + } else { + dir = &configdir.Config{} + dir.Path = filepath.Dir(conf.ConfigFile) + file = filepath.Base(conf.ConfigFile) } marshalled := marshal(conf) - err := ds[0].WriteFile(cfn, marshalled) + err := dir.WriteFile(file, marshalled) if err != nil { return "", err } - return filepath.Join(ds[0].Path, cfn), nil + return filepath.Join(dir.Path, file), nil } func marshal(c *Config) []byte { diff --git a/go.mod b/go.mod index 29a5919..d0867eb 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/stretchr/testify v1.4.0 github.com/xxxserxxx/iSMC v1.0.1 - github.com/xxxserxxx/opflag v1.0.3 + github.com/xxxserxxx/opflag v1.0.5 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect golang.org/x/tools v0.0.0-20200425043458-8463f397d07c // indirect golang.org/x/tools/gopls v0.4.0 // indirect diff --git a/go.sum b/go.sum index 2410a27..f165c4a 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,10 @@ github.com/xxxserxxx/opflag v1.0.2 h1:TanW4Ck/RNal4fP2VVAvhEu7eBq4z+9hhGq9Q8OTq6 github.com/xxxserxxx/opflag v1.0.2/go.mod h1:GWZtb3/tGGj5W1GE/JTyJAuqgxDxl1+jqDGAGM+P/p4= github.com/xxxserxxx/opflag v1.0.3 h1:ugsBWZtSXUaMLEjPLW0WKGjq/gsrc0GpZYbCJY6ZHVY= github.com/xxxserxxx/opflag v1.0.3/go.mod h1:GWZtb3/tGGj5W1GE/JTyJAuqgxDxl1+jqDGAGM+P/p4= +github.com/xxxserxxx/opflag v1.0.4 h1:g979b8oReAERfDKFTTwdvAYIarFxpVYOzYrHa/hMvNs= +github.com/xxxserxxx/opflag v1.0.4/go.mod h1:GWZtb3/tGGj5W1GE/JTyJAuqgxDxl1+jqDGAGM+P/p4= +github.com/xxxserxxx/opflag v1.0.5 h1:2H4Qtl1qe+dSkEcGt+fBe2mQ8z14MgkWPqcLaoa6k90= +github.com/xxxserxxx/opflag v1.0.5/go.mod h1:GWZtb3/tGGj5W1GE/JTyJAuqgxDxl1+jqDGAGM+P/p4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= diff --git a/widgets/temp.go b/widgets/temp.go index 05ad3c1..f2b0f93 100644 --- a/widgets/temp.go +++ b/widgets/temp.go @@ -31,7 +31,6 @@ type TempWidget struct { tempsMetric map[string]prometheus.Gauge } -// TODO: state:deferred 156 Added temperatures for NVidia GPUs (azak-azkaran/master). Crashes on non-nvidia machines. func NewTempWidget(tempScale TempScale, filter []string) *TempWidget { self := &TempWidget{ Block: ui.NewBlock(),