package gotop import ( "bufio" "bytes" "embed" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "strconv" "strings" "time" "github.com/shibukawa/configdir" "github.com/xxxserxxx/gotop/v4/colorschemes" "github.com/xxxserxxx/gotop/v4/widgets" "github.com/xxxserxxx/lingo/v2" ) // FIXME github action uses old(er) Go version that doesn't have embed //go:embed "dicts/*.toml" var Dicts embed.FS // CONFFILE is the name of the default config file const CONFFILE = "gotop.conf" type Config struct { ConfigDir configdir.ConfigDir GraphHorizontalScale int HelpVisible bool Colorscheme colorschemes.Colorscheme UpdateInterval time.Duration AverageLoad bool NoAverageLoad bool PercpuLoad bool NoPercpuLoad bool Statusbar bool NoStatusbar bool TempScale widgets.TempScale NetInterface string Layout string MaxLogSize int64 ExportPort string Mbps bool NoMbps bool Temps []string Test bool NoTest bool ExtensionVars map[string]string ConfigFile string Tr lingo.Translations Nvidia bool NoNvidia bool NvidiaRefresh time.Duration } // FIXME parsing can't handle blank lines func NewConfig() Config { cd := configdir.New("", "gotop") cd.LocalPath, _ = filepath.Abs(".") conf := Config{ ConfigDir: cd, GraphHorizontalScale: 7, HelpVisible: false, UpdateInterval: time.Second, AverageLoad: false, NoAverageLoad: false, PercpuLoad: true, NoPercpuLoad: false, TempScale: widgets.Celsius, Statusbar: false, NoStatusbar: false, NoMbps: false, NoTest: false, NoNvidia: false, NetInterface: widgets.NetInterfaceAll, MaxLogSize: 5000000, Layout: "default", 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 if conf.ConfigFile == "" { return nil } var err error if _, err = os.Stat(conf.ConfigFile); os.IsNotExist(err) { // Check for the file in the usual suspects folder := conf.ConfigDir.QueryFolderContainsFile(conf.ConfigFile) if folder == nil { return nil } conf.ConfigFile = filepath.Join(folder.Path, conf.ConfigFile) } if in, err = ioutil.ReadFile(conf.ConfigFile); err != nil { return err } return load(bytes.NewReader(in), conf) } func load(in io.Reader, conf *Config) error { r := bufio.NewScanner(in) var lineNo int for r.Scan() { l := strings.TrimSpace(r.Text()) if l[0] == '#' { continue } kv := strings.Split(l, "=") if len(kv) != 2 { return fmt.Errorf(conf.Tr.Value("config.err.configsyntax", l)) } key := strings.ToLower(kv[0]) ln := strconv.Itoa(lineNo) switch key { default: conf.ExtensionVars[key] = kv[1] case "configdir", "logdir", "logfile": log.Printf(conf.Tr.Value("config.err.deprecation", ln, key, kv[1])) case graphhorizontalscale: iv, err := strconv.Atoi(kv[1]) if err != nil { return err } conf.GraphHorizontalScale = iv case helpvisible: bv, err := strconv.ParseBool(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.HelpVisible = bv case colorscheme: cs, err := colorschemes.FromName(conf.ConfigDir, kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.Colorscheme = cs case updateinterval: iv, err := strconv.Atoi(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.UpdateInterval = time.Duration(iv) case averagecpu: bv, err := strconv.ParseBool(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.AverageLoad = bv case percpuload: bv, err := strconv.ParseBool(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.PercpuLoad = bv case tempscale: switch kv[1] { case "C": conf.TempScale = 'C' case "F": conf.TempScale = 'F' default: conf.TempScale = 'C' return fmt.Errorf(conf.Tr.Value("config.err.tempscale", kv[1])) } case statusbar: bv, err := strconv.ParseBool(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.Statusbar = bv case netinterface: conf.NetInterface = kv[1] case layout: conf.Layout = kv[1] case maxlogsize: iv, err := strconv.Atoi(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.MaxLogSize = int64(iv) case export: conf.ExportPort = kv[1] case mbps: conf.Mbps = true case temperatures: conf.Temps = strings.Split(kv[1], ",") case nvidia: nv, err := strconv.ParseBool(kv[1]) if err != nil { return fmt.Errorf(conf.Tr.Value("config.err.line", ln, err.Error())) } conf.Nvidia = nv } } 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) { var dir *configdir.Config var file string = CONFFILE if conf.ConfigFile == "" { ds := conf.ConfigDir.QueryFolders(configdir.Global) if len(ds) == 0 { 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 := dir.WriteFile(file, marshalled) if err != nil { return "", err } return filepath.Join(dir.Path, file), nil } func marshal(c *Config) []byte { buff := bytes.NewBuffer(nil) fmt.Fprintln(buff, "# Scale graphs to this level; 7 is the default, 2 is zoomed out.") fmt.Fprintf(buff, "%s=%d\n", graphhorizontalscale, c.GraphHorizontalScale) fmt.Fprintln(buff, "# If true, start the UI with the help visible") fmt.Fprintf(buff, "%s=%t\n", helpvisible, c.HelpVisible) fmt.Fprintln(buff, "# The color scheme to use. See `--list colorschemes`") fmt.Fprintf(buff, "%s=%s\n", colorscheme, c.Colorscheme.Name) fmt.Fprintln(buff, "# How frequently to update the UI, in nanoseconds") fmt.Fprintf(buff, "%s=%d\n", updateinterval, c.UpdateInterval) fmt.Fprintln(buff, "# If true, show the average CPU load") fmt.Fprintf(buff, "%s=%t\n", averagecpu, c.AverageLoad) fmt.Fprintln(buff, "# If true, show load per CPU") fmt.Fprintf(buff, "%s=%t\n", percpuload, c.PercpuLoad) fmt.Fprintln(buff, "# Temperature units. C for Celcius, F for Fahrenheit") fmt.Fprintf(buff, "%s=%c\n", tempscale, c.TempScale) fmt.Fprintln(buff, "# If true, display a status bar") fmt.Fprintf(buff, "%s=%t\n", statusbar, c.Statusbar) fmt.Fprintln(buff, "# The network interface to monitor") fmt.Fprintf(buff, "%s=%s\n", netinterface, c.NetInterface) fmt.Fprintln(buff, "# A layout name. See `--list layouts`") fmt.Fprintf(buff, "%s=%s\n", layout, c.Layout) fmt.Fprintln(buff, "# The maximum log file size, in bytes") fmt.Fprintf(buff, "%s=%d\n", maxlogsize, c.MaxLogSize) fmt.Fprintln(buff, "# If set, export data as Promethius metrics on the interface:port.\n# E.g., `:8080` (colon is required, interface is not)") if c.ExportPort == "" { fmt.Fprint(buff, "#") } fmt.Fprintf(buff, "%s=%s\n", export, c.ExportPort) fmt.Fprintln(buff, "# Display network IO in mpbs if true") fmt.Fprintf(buff, "%s=%t\n", mbps, c.Mbps) fmt.Fprintln(buff, "# A list of enabled temp sensors. See `--list devices`") if len(c.Temps) == 0 { fmt.Fprint(buff, "#") } fmt.Fprintf(buff, "%s=%s\n", temperatures, strings.Join(c.Temps, ",")) fmt.Fprintln(buff, "# Enable NVidia GPU metrics.") fmt.Fprintf(buff, "%s=%t\n", nvidia, c.Nvidia) fmt.Fprintln(buff, "# To configure the NVidia refresh rate, set a duration:") fmt.Fprintln(buff, "#nvidiarefresh=30s") return buff.Bytes() } const ( graphhorizontalscale = "graphhorizontalscale" helpvisible = "helpvisible" colorscheme = "colorscheme" updateinterval = "updateinterval" averagecpu = "averagecpu" percpuload = "percpuload" tempscale = "tempscale" statusbar = "statusbar" netinterface = "netinterface" layout = "layout" maxlogsize = "maxlogsize" export = "metricsexportport" mbps = "mbps" temperatures = "temperatures" nvidia = "nvidia" )