// Package iconTheme implements icon-theme-spec version 0.13. package iconTheme import "os" import "io" import "fmt" import "log" import "image" import "path/filepath" import "git.tebibyte.media/tomo/xdg/basedir" import "git.tebibyte.media/tomo/xdg/key-value" type iconThemeError string func (err iconThemeError) Error () string { return string(err) } const ( // ErrThemeNotFound indicates that a theme could not be found. ErrThemeNotFound = iconThemeError("theme not found") // ErrGroupMissing indicates that a required group was not found in an // index.theme file. ErrGroupMissing = iconThemeError("group missing") // ErrIconThemeGroupMissing indicates that the [Icon Theme] group was // not found while parsing an index.theme file. ErrIconThemeGroupMissing = iconThemeError("icon theme group missing") // ErrDirectoryGroupMissing indicates that a directory group was not // found while parsing an index.Theme file. ErrDirectoryGroupMissing = iconThemeError("directory group missing") // ErrEntryMissing indicates that a required entry was not found in an // index.theme file. ErrEntryMissing = iconThemeError("entry missing") // ErrBadIconSizeType indicates that an invalid value for an icon size // type was given. ErrBadIconSizeType = iconThemeError("bad icon size type") // ErrCircularDependency indicates that an icon theme depends on one of // its dependents. ErrCircularDependency = iconThemeError("circular dependency") ) // ThemeDirs returns the set of directories in which themes should be looked for // in order of preference. // // It will return $HOME/.icons (for backwards compatibility), in // $XDG_DATA_DIRS/icons and in /usr/share/pixmaps. Applications may further add // their own icon directories to this list. func ThemeDirs () ([]string, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } dataDirs, err := basedir.DataDirs() if err != nil { return nil, err } dirs := make([]string, len(dataDirs) + 2) dirs[0] = filepath.Join(home, ".icons") dirs[len(dirs) - 1] = "/usr/share/pixmaps" for index, dataDir := range dataDirs { dirs[index + 1] = filepath.Join(dataDir, "icons") } return dirs, nil } // FindTheme returns the theme of the given name located within the given search // paths. If no search path is provided, the result of ThemeDirs is used. This // function will also recursively find and read inherited themes. If an // inherited theme is malformed and cannot be read, hicolor will be used // instead. The directory structure and inheritence tree of a theme are only // determined when this function is run, if a theme has been changed and needs // to be reloaded this function should be called again. func FindTheme (name string, path ...string) (Theme, error) { return findTheme(name, nil, false, path...) } // FindThemeWarn is like FindTheme, but emits warnings using the log package if // it encounters a recoverable error (such as failing to parse/find an inherited // theme). func FindThemeWarn (name string, path ...string) (Theme, error) { return findTheme(name, nil, true, path...) } func findTheme (name string, trail []string, warn bool, path ...string) (Theme, error) { for _, step := range trail { if step == name { return Theme { }, ErrCircularDependency } } if len(path) == 0 { themeDirs, err := ThemeDirs() if err != nil { return Theme { }, nil } path = themeDirs } // find paths to all instances of this theme instances := []string { } for _, dir := range path { entries, err := os.ReadDir(dir) if err != nil { return Theme { }, err } for _, entry := range entries { if entry.Name() == name { instances = append ( instances, filepath.Join(dir, entry.Name())) break } } } if len(instances) == 0 { return Theme { }, ErrThemeNotFound } // find an index.theme var lastParseErr error var theme Theme var parents []string for _, instance := range instances { indexPath := filepath.Join(instance, "index.theme") file, err := os.Open(indexPath) lastParseErr = err if err != nil { continue } defer file.Close() theme, parents, err = parseThemeIndex(file) lastParseErr = err if err != nil { continue } break } if lastParseErr != nil { return Theme { }, lastParseErr } // expand directories to cover all instances for index, directory := range theme.Directories { directory.Paths = make([]string, len(instances)) for pathIndex, path := range directory.Paths { directory.Paths[pathIndex] = filepath.Join(path, directory.Name) } theme.Directories[index] = directory } // this will not stomp memory its fine trail = append(trail, name) // parse parents for _, parent := range parents { parentTheme, err := findTheme(parent, trail, warn, path...) if err != nil { if warn { log.Printf ( "xdg: could not parse inherited theme %s: %v", parent, err) } continue } theme.Inherits = append(theme.Inherits, parentTheme) } // if no parents were successfuly parsed, inherit from hicolor if len(theme.Inherits) == 0 && name != "hicolor" { hicolor, err := findTheme("hicolor", trail, warn, path...) if err != nil { return Theme { }, err } theme.Inherits = []Theme { hicolor } } return theme, nil } func parseThemeIndex (reader io.Reader) (theme Theme, parents []string, err error) { file, err := keyValue.Parse(reader) if err != nil { return Theme { }, nil, err } iconThemeGroup, ok := file["Icon Theme"] if !ok { return Theme { }, nil, ErrIconThemeGroupMissing } // Name entry, ok := iconThemeGroup["Name"] if !ok { return Theme { }, nil, ErrEntryMissing } theme.Name, err = entry.Unescape() if err != nil { return Theme { }, nil, err } // Comment entry, ok = iconThemeGroup["Comment"] if !ok { return Theme { }, nil, ErrEntryMissing } theme.Comment, err = entry.Unescape() if err != nil { return Theme { }, nil, err } // Inherits (optional) if entry, ok := iconThemeGroup["Inherits"]; ok { parents, err = keyValue.ParseMultipleComma ( keyValue.ParseString, entry.Value) if !ok { return Theme { }, nil, err } } // Directories entry, ok = iconThemeGroup["Directories"] if !ok { return Theme { }, nil, ErrEntryMissing } theme.Directories, err = parseDirectories(entry, file) if err != nil { return Theme { }, nil, err } // ScaledDirectories (optional) if entry, ok := iconThemeGroup["ScaledDirectories"]; ok { theme.ScaledDirectories, err = parseDirectories(entry, file) if err != nil { return Theme { }, nil, err } } // Hidden (optional) if entry, ok := iconThemeGroup["Hidden"]; ok { theme.Hidden, err = keyValue.ParseBoolean(entry.Value) if err != nil { return Theme { }, nil, err } } // Example (optional) if entry, ok := iconThemeGroup["Example"]; ok { theme.Example, err = keyValue.ParseString(entry.Value) if err != nil { return Theme { }, nil, err } } return theme, parents, nil } func parseDirectories (listEntry keyValue.Entry, file keyValue.File) ([]Directory, error) { names, err := keyValue.ParseMultipleComma(keyValue.ParseString, listEntry.Value) if err != nil { return nil, err } directories := make([]Directory, len(names)) for index, name := range names { directoryGroup, ok := file[name] if !ok { return nil, ErrDirectoryGroupMissing } directory := Directory { Name: name, } // Size entry, ok := directoryGroup["Size"] if !ok { return nil, ErrEntryMissing } directory.Size, err = keyValue.ParseInteger(entry.Value) if err != nil { return nil, err } // Scale (optional) if entry, ok := directoryGroup["Scale"]; ok { directory.Scale, err = keyValue.ParseInteger(entry.Value) if err != nil { return nil, err } } else { directory.Scale = 1 } // Context (optional) if entry, ok := directoryGroup["Context"]; ok { directory.Context, err = keyValue.ParseString(entry.Value) if err != nil { return nil, err } } // Type (optional) if entry, ok := directoryGroup["Type"]; ok { stringTy, err := keyValue.ParseString(entry.Value) if err != nil { return nil, err } directory.Type, err = IconSizeTypeFromString(stringTy) if err != nil { return nil, err } } else { directory.Type = IconSizeTypeThreshold } // MaxSize (optional) if entry, ok := directoryGroup["MaxSize"]; ok { directory.MaxSize, err = keyValue.ParseInteger(entry.Value) if err != nil { return nil, err } } else { directory.MaxSize = directory.Size } // MinSize (optional) if entry, ok := directoryGroup["MinSize"]; ok { directory.MinSize, err = keyValue.ParseInteger(entry.Value) if err != nil { return nil, err } } else { directory.MinSize = directory.Size } // Threshold (optional) if entry, ok := directoryGroup["Threshold"]; ok { directory.Threshold, err = keyValue.ParseInteger(entry.Value) if err != nil { return nil, err } } else { directory.Threshold = 2 } directories[index] = directory } return directories, nil } // Theme represents an icon theme. type Theme struct { Name keyValue.Entry Comment keyValue.Entry Inherits []Theme Directories []Directory ScaledDirectories []Directory Hidden bool Example string } // Directory represents a directory of an icon theme which directly contains // icons. type Directory struct { Name string Paths []string Size int Scale int Context string Type IconSizeType MaxSize int MinSize int Threshold int } // IconSizeType specifies how an icon can be scaled. Fixed icons cannot be // scaled, scalable icons can be scaled to any size within a min and max range, // and threshold icons can be scaled to any size within a certain distance of // their normal size. type IconSizeType int; const ( IconSizeTypeFixed IconSizeType = iota IconSizeTypeScalable IconSizeTypeThreshold ) // String returns a string representation of the type. func (ty IconSizeType) String () string { switch ty { case IconSizeTypeFixed: return "Fixed" case IconSizeTypeScalable: return "Scalable" case IconSizeTypeThreshold: return "Threshold" default: return fmt.Sprintf("IconSizeType(%d)", ty) } } // IconSizeTypeFromString converts a astring into an IconSizeType, returning an // error if it could not be converted. func IconSizeTypeFromString (value string) (IconSizeType, error) { switch value { case "Fixed": return IconSizeTypeFixed, nil case "Scalable": return IconSizeTypeScalable, nil case "Threshold": return IconSizeTypeThreshold, nil default: return 0, ErrBadIconSizeType } } // Icon represents data in .icon files type Icon struct { DisplayName string EmbeddedTextRectangle image.Rectangle AttachPoints []image.Point }