Add icon theme parsing
This commit is contained in:
parent
51056714f1
commit
c3487af9b8
335
icon-theme/icon-theme.go
Normal file
335
icon-theme/icon-theme.go
Normal file
@ -0,0 +1,335 @@
|
||||
// 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 (
|
||||
// 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 path == nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(name, 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 theme.Inherits == nil {
|
||||
hicolor, err := findTheme(name, 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.ParseMultiple (
|
||||
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
|
||||
if entry, ok := iconThemeGroup["ScaledDirectories"]; ok {
|
||||
theme.ScaledDirectories, err = parseDirectories(entry, file)
|
||||
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-name>.icon files
|
||||
type Icon struct {
|
||||
DisplayName string
|
||||
EmbeddedTextRectangle image.Rectangle
|
||||
AttachPoints []image.Point
|
||||
}
|
Loading…
Reference in New Issue
Block a user