package config import "io" import "os" import "bufio" import "strings" import "strconv" import "image/color" import "path/filepath" // when making changes to this file, look at // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html // Error represents an error that can be returned by functions or methods in // this module. type Error int const ( // ErrorIllegalName is thrown when an application name contains illegal // characters such as a slash. ErrorIllegalName Error = iota // ErrorNoSeparator is thrown when a configuration file has an // incorrectly formatted key-value pair. ErrorNoSeparator // ErrorUnknownParameter is thrown when an unknown key is encountered in // a configuration file. ErrorUnknownParameter // ErrorWrongColorLength is thrown when a configuration file has a color // literal with a total length unequal to 7. ErrorWrongColorLength // ErrorMalformedColorLiteral is thrown when a configuration file has an // improperly formatted color literal, or a color literal was expected // and something else was encountered. ErrorMalformedColorLiteral // ErrorMalformedIntegerLiteral is thrown when a configuration file has // an improperly formatted integer literal, or an integer literal was // expected and something else was encountered. ErrorMalformedIntegerLiteral // ErrorMalformedFloatLiteral is thrown when a configuration file has // an improperly formatted float literal, or a float literal was // expected and something else was encountered. ErrorMalformedFloatLiteral ) // Error returns a description of the error. func (err Error) Error () (description string) { switch err { case ErrorIllegalName: description = "name contains illegal characters" case ErrorNoSeparator: description = "key:value pair has no separator" case ErrorUnknownParameter: description = "unknown parameter" case ErrorWrongColorLength: description = "color literal has the wrong length" case ErrorMalformedColorLiteral: description = "malformed color literal" case ErrorMalformedIntegerLiteral: description = "malformed integer literal" case ErrorMalformedFloatLiteral: description = "malformed float literal" } return } // Type represents the data type of a configuration parameter. type Type int const ( // string // It is just a basic string with inner whitespace preserved. No quotes // should be used in the file. TypeString Type = iota // Type: image/color.RGBA // Represented as a 24 bit hexadecimal number (case insensitive) // preceded with a # sign where the first two digits represent the red // channel, the middle two digits represent the green channel, and the // last two digits represent the blue channel. TypeColor // Type: int // An integer literal, like 123456789 TypeInteger // Type: float64 // A floating point literal, like 1234.56789 TypeFloat // Type: bool // Values true, yes, on, and 1 are all truthy (case insensitive) and // anything else is falsy. TypeBoolean ) // Config holds a list of configuration parameters. type Config struct { // LegalParameters holds the names and types of all parameters that can // be parsed. If the parser runs into a parameter that is not listed // here, it will print out an error message and keep on parsing. LegalParameters map[string] Type // Parameters holds the names and values of all parsed parameters. If a // value is non-nil, it can be safely type asserted into whatever type // was requested. Parameters map[string] any } // Load loads and parses the files /etc/xdg//.conf and // /.config//.conf, unless the corresponding XDG environment // variables are set - then it uses those. Configuration files are divided into // lines where each line may be blank, a comment, or a key-value pair. If the // line is blank or begins with a # character, it is ignored. Else, the line // must have a key and a value separated by a colon. Before they are processed, // leading and trailing whitespace is trimmed from the key and the value. Keys // are case sensitive. func (config *Config) Load (name string) (err error) { if strings.ContainsAny(name, "/\\|:.%") { err = ErrorIllegalName return } if config.LegalParameters == nil { config.LegalParameters = make(map[string] Type) } if config.Parameters == nil { config.Parameters = make(map[string] any) } var homeDirectory string homeDirectory, err = os.UserHomeDir() if err != nil { return } configHome := os.Getenv("XDG_CONFIG_HOME") if configHome == "" { configHome = filepath.Join(homeDirectory, "/.config/") } configDirsString := os.Getenv("XDG_CONFIG_DIRS") if configDirsString == "" { configDirsString = "/etc/xdg/" } configDirs := strings.Split(configDirsString, ":") configDirs = append(configDirs, configHome) for _, directory := range configDirs { path := filepath.Join(directory, name, name + ".conf") file, fileErr := os.Open(path) if fileErr != nil { continue } parseErr := config.loadFile(file) defer file.Close() if parseErr != nil { println ( "config: error in file", path + ":", parseErr.Error()) } } return } func (config *Config) loadFile (file io.Reader) (err error) { scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if len(line) == 0 { continue } if line[0] == '#' { continue } key, value, found := strings.Cut(scanner.Text(), ":") if !found { err = ErrorNoSeparator return } key = strings.TrimSpace(key) value = strings.TrimSpace(value) what, isKnown := config.LegalParameters[key] if !isKnown { err = ErrorUnknownParameter return } switch what { case TypeString: config.Parameters[key] = value case TypeColor: var valueColor color.Color valueColor, err = parseColor(value) if err != nil { return } config.Parameters[key] = valueColor case TypeInteger: var valueInt int valueInt, err = strconv.Atoi(value) if err != nil { err = ErrorMalformedIntegerLiteral return } config.Parameters[key] = valueInt case TypeFloat: var valueFloat float64 valueFloat, err = strconv.ParseFloat(value, 64) if err != nil { err = ErrorMalformedFloatLiteral return } config.Parameters[key] = valueFloat case TypeBoolean: value = strings.ToLower(value) truthy := value == "true" || value == "yes" || value == "on" || value == "1" config.Parameters[key] = truthy } } return } func parseColor (value string) (valueColor color.Color, err error) { if value[0] == '#' { if len(value) != 7 { err = ErrorWrongColorLength return } var colorInt uint64 colorInt, err = strconv.ParseUint(value[1:7], 16, 24) if err != nil { err = ErrorMalformedColorLiteral return } valueColor = color.RGBA { R: uint8(colorInt >> 16), G: uint8(colorInt >> 8), B: uint8(colorInt), A: 0xFF, } } else { err = ErrorMalformedColorLiteral return } return }