Compare commits
16 Commits
73ea9d304b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 66c337da2a | |||
| e5d6091000 | |||
| c0e5cb7db3 | |||
| 39e6d0187e | |||
| c465ff6d1e | |||
| 19b442598d | |||
| e9ff323f69 | |||
| 8b4f77b223 | |||
| 3b1620b5ef | |||
| 5b9c1ed1ef | |||
| bc7b9c58a8 | |||
| d82e802563 | |||
| a7a3e8b675 | |||
| c3487af9b8 | |||
| 51056714f1 | |||
| bdbb90fd32 |
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 8
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -3,6 +3,4 @@
|
|||||||
[](https://pkg.go.dev/git.tebibyte.media/tomo/xdg)
|
[](https://pkg.go.dev/git.tebibyte.media/tomo/xdg)
|
||||||
|
|
||||||
This module seeks to implement parts of the XDG specification that do not rely
|
This module seeks to implement parts of the XDG specification that do not rely
|
||||||
on other software. Parts of the specification that do, eg. mpris-spec and
|
on other software.
|
||||||
notifcation-spec, may in the future be implemented in a separate module that
|
|
||||||
depends on this one.
|
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ func listDefault (list string, defaul ...string) []string {
|
|||||||
|
|
||||||
index := 0
|
index := 0
|
||||||
for _, dir := range envDirs {
|
for _, dir := range envDirs {
|
||||||
if Valid(dir) != nil {
|
if Valid(dir) == nil {
|
||||||
dirs[index] = dir
|
dirs[index] = dir
|
||||||
index ++
|
index ++
|
||||||
}
|
}
|
||||||
|
|||||||
548
icon-theme/icon-theme.go
Normal file
548
icon-theme/icon-theme.go
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
// Package iconTheme implements icon-theme-spec version 0.13.
|
||||||
|
package iconTheme
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
import "io"
|
||||||
|
import "fmt"
|
||||||
|
import "log"
|
||||||
|
import "image"
|
||||||
|
import "strings"
|
||||||
|
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")
|
||||||
|
// ErrIconNotFound indicates that an icon could not be found.
|
||||||
|
ErrIconNotFound = iconThemeError("icon not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 { continue }
|
||||||
|
|
||||||
|
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 (
|
||||||
|
instances[pathIndex],
|
||||||
|
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.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 (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.ParseMultiple(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindIcon finds the best icon of the given name for the given size and scale.
|
||||||
|
// If a list of supported filetypes is not given, { PNG, SVG, XPM } is used
|
||||||
|
// instead.
|
||||||
|
func (theme Theme) FindIcon (name string, size, scale int, supported ...IconFileType) (Icon, error) {
|
||||||
|
// this method is equivalent to FindIconHelper
|
||||||
|
if len(supported) == 0 { supported = []IconFileType{ PNG, SVG, XPM } }
|
||||||
|
|
||||||
|
// try to find icon in this theme first
|
||||||
|
icon, err := theme.findIconHere(name, size, scale, supported...)
|
||||||
|
if err == nil { return icon, nil }
|
||||||
|
|
||||||
|
// if that didn't work, recurse through parent themes
|
||||||
|
for _, parent := range theme.Inherits {
|
||||||
|
icon, err := parent.FindIcon(name, size, scale, supported...)
|
||||||
|
if err == nil { return icon, nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon { }, ErrIconNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme Theme) findIconHere (name string, size, scale int, supported ...IconFileType) (Icon, error) {
|
||||||
|
// this method is equivalent to LookupIcon
|
||||||
|
|
||||||
|
// stage 1: attempt to find an exact match
|
||||||
|
for _, directory := range theme.Directories {
|
||||||
|
if directory.Fits(size, scale) {
|
||||||
|
for _, path := range directory.Paths {
|
||||||
|
for _, ty := range supported {
|
||||||
|
icon, err := parseIcon(filepath.Join (
|
||||||
|
path, ty.Extend(name)))
|
||||||
|
if err == nil { return icon, err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage 2: find the best icon matching the name
|
||||||
|
minimalDistance := int((^uint(0)) >> 1)
|
||||||
|
closestFilename := ""
|
||||||
|
for _, directory := range theme.Directories {
|
||||||
|
sizeDistance := directory.SizeDistance(size, scale)
|
||||||
|
if sizeDistance < minimalDistance {
|
||||||
|
for _, path := range directory.Paths {
|
||||||
|
for _, ty := range supported {
|
||||||
|
iconFilename := filepath.Join (
|
||||||
|
path, ty.Extend(name))
|
||||||
|
if _, err := os.Stat(iconFilename); err == nil {
|
||||||
|
closestFilename = iconFilename
|
||||||
|
minimalDistance = sizeDistance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if closestFilename != "" {
|
||||||
|
icon, err := parseIcon(closestFilename)
|
||||||
|
if err == nil { return icon, err }
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon { }, ErrIconNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fits returns whether icons in the directory fit the given size and scale.
|
||||||
|
func (directory Directory) Fits (size, scale int) bool {
|
||||||
|
// this method is equivalent to DirectoryMatchesSize
|
||||||
|
if directory.Scale != scale { return false }
|
||||||
|
switch directory.Type {
|
||||||
|
case IconSizeTypeFixed:
|
||||||
|
return directory.Size == size
|
||||||
|
case IconSizeTypeScalable:
|
||||||
|
return directory.MinSize <= size && size <= directory.MaxSize
|
||||||
|
case IconSizeTypeThreshold:
|
||||||
|
return directory.Size - directory.Threshold <=
|
||||||
|
size && size <=
|
||||||
|
directory.Size + directory.Threshold
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizeDistance returns how close the size of this directory's icons is to the
|
||||||
|
// given size. A smaller number is closer.
|
||||||
|
func (directory Directory) SizeDistance (size, scale int) int {
|
||||||
|
// this method is equivalent to DirectorySizeDistance
|
||||||
|
// TODO: the pseudocode for DirectorySizeDistance in the spec seems
|
||||||
|
// incredibly wrong, and the code here has been altered to correct for
|
||||||
|
// that. figure out why the spec is the way it is.
|
||||||
|
scaledSize := size * scale
|
||||||
|
scaledDir := directory.Size * directory.Scale
|
||||||
|
scaledMin := directory.MinSize * directory.Scale
|
||||||
|
scaledMax := directory.MaxSize * directory.Scale
|
||||||
|
threshMin := (directory.Size - directory.Threshold) * directory.Scale
|
||||||
|
threshMax := (directory.Size + directory.Threshold) * directory.Scale
|
||||||
|
|
||||||
|
switch directory.Type {
|
||||||
|
case IconSizeTypeFixed:
|
||||||
|
difference := scaledDir - scaledSize
|
||||||
|
if difference > 0 {
|
||||||
|
return difference
|
||||||
|
} else {
|
||||||
|
return -difference
|
||||||
|
}
|
||||||
|
|
||||||
|
case IconSizeTypeScalable:
|
||||||
|
if scaledSize < scaledMin { return scaledMin - scaledSize }
|
||||||
|
if scaledSize > scaledMax { return scaledSize - scaledMax }
|
||||||
|
|
||||||
|
case IconSizeTypeThreshold:
|
||||||
|
if scaledSize < threshMin { return threshMin - scaledSize }
|
||||||
|
if scaledSize > threshMax { return scaledSize - threshMin }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IconFileType specifies the file type of an icon.
|
||||||
|
type IconFileType uint; const (
|
||||||
|
PNG IconFileType = iota
|
||||||
|
SVG
|
||||||
|
XPM
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a string representation of the file type.
|
||||||
|
func (ty IconFileType) String () string {
|
||||||
|
switch ty {
|
||||||
|
case PNG: return ".png"
|
||||||
|
case SVG: return ".svg"
|
||||||
|
case XPM: return ".xpm"
|
||||||
|
default: return fmt.Sprintf("IconFileType(%d)", ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend applies the file extension to a file name.
|
||||||
|
func (ty IconFileType) Extend (name string) string {
|
||||||
|
return name + ty.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 an icon, and its associated data (if applicable).
|
||||||
|
type Icon struct {
|
||||||
|
Path string
|
||||||
|
FileType IconFileType
|
||||||
|
DisplayName keyValue.Entry
|
||||||
|
EmbeddedTextRectangle image.Rectangle
|
||||||
|
AttachPoints []image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIcon (path string) (Icon, error) {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
dataPath := strings.TrimSuffix(path, ext) + ".icon"
|
||||||
|
|
||||||
|
if data, err := parseIconData(dataPath); err != nil {
|
||||||
|
data.Path = path
|
||||||
|
return data, nil
|
||||||
|
} else {
|
||||||
|
return Icon {
|
||||||
|
Path: path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIconData (path string) (Icon, error) {
|
||||||
|
reader, err := os.Open(path)
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
file, err := keyValue.Parse(reader)
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
|
||||||
|
iconDataGroup, ok := file["Icon Data"]
|
||||||
|
if !ok { return Icon { }, ErrGroupMissing }
|
||||||
|
|
||||||
|
icon := Icon { }
|
||||||
|
|
||||||
|
// DisplayName (optional)
|
||||||
|
if entry, ok := iconDataGroup["DisplayName"]; ok {
|
||||||
|
icon.DisplayName, err = entry.Unescape()
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedTextRectangle (optional)
|
||||||
|
if entry, ok := iconDataGroup["EmbeddedTextRectangle"]; ok {
|
||||||
|
embeddedTextRectangle, err := keyValue.ParseMultiple(keyValue.ParseInteger, entry.Value, ',')
|
||||||
|
if len(embeddedTextRectangle) == 4 {
|
||||||
|
icon.EmbeddedTextRectangle = image.Rect (
|
||||||
|
embeddedTextRectangle[0],
|
||||||
|
embeddedTextRectangle[1],
|
||||||
|
embeddedTextRectangle[2],
|
||||||
|
embeddedTextRectangle[3])
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO figure out what the actual fuck the spec is talking about with
|
||||||
|
// regard to AttatchPoints. What even the fuck is that supposed to mean.
|
||||||
|
// Ohhhh a list of points separated by |. Mf hoe what do you define as
|
||||||
|
// a point.
|
||||||
|
|
||||||
|
return icon, nil
|
||||||
|
}
|
||||||
@@ -64,9 +64,21 @@ func Parse (reader io.Reader) (File, error) {
|
|||||||
var file = File { }
|
var file = File { }
|
||||||
var group Group
|
var group Group
|
||||||
var specifiedValues map[string] struct { }
|
var specifiedValues map[string] struct { }
|
||||||
|
|
||||||
|
checkGroupIntegrity := func () error {
|
||||||
|
if group != nil {
|
||||||
|
for key := range group {
|
||||||
|
if _, ok := specifiedValues[key]; !ok {
|
||||||
|
return ErrNoDefaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, err := buffer.ReadString('\n')
|
line, err := buffer.ReadString('\n')
|
||||||
if errors.Is(err, io.EOF) { return file, nil }
|
if errors.Is(err, io.EOF) { break }
|
||||||
if err != nil { return nil, err }
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
@@ -78,13 +90,8 @@ func Parse (reader io.Reader) (File, error) {
|
|||||||
// group header
|
// group header
|
||||||
case strings.HasPrefix(line, "["):
|
case strings.HasPrefix(line, "["):
|
||||||
// check integrity of prev. group
|
// check integrity of prev. group
|
||||||
if group != nil {
|
err := checkGroupIntegrity()
|
||||||
for key := range group {
|
if err != nil { return File { }, err }
|
||||||
if _, ok := specifiedValues[key]; !ok {
|
|
||||||
return nil, ErrNoDefaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new group
|
// create new group
|
||||||
name, err := parseGroupHeader(line)
|
name, err := parseGroupHeader(line)
|
||||||
@@ -109,15 +116,23 @@ func Parse (reader io.Reader) (File, error) {
|
|||||||
group[key] = entry
|
group[key] = entry
|
||||||
} else {
|
} else {
|
||||||
// default value
|
// default value
|
||||||
_, exists := group[key]
|
_, specified := specifiedValues[key]
|
||||||
if exists { return nil, ErrDuplicateEntry }
|
if specified { return nil, ErrDuplicateEntry }
|
||||||
entry := newEntry()
|
|
||||||
|
entry, exists := group[key]
|
||||||
|
if !exists { entry = newEntry() }
|
||||||
|
|
||||||
entry.Value = value
|
entry.Value = value
|
||||||
group[key] = entry
|
group[key] = entry
|
||||||
specifiedValues[key] = struct { } { }
|
specifiedValues[key] = struct { } { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := checkGroupIntegrity()
|
||||||
|
if err != nil { return File { }, err }
|
||||||
|
|
||||||
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEntry () Entry {
|
func newEntry () Entry {
|
||||||
@@ -250,7 +265,23 @@ func (entry Entry) Localize (locale locale.Locale) string {
|
|||||||
return entry.Value
|
return entry.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO have functions to parse/validate all data types
|
// Unescape returns a new copy of this entry with its main value parsed and
|
||||||
|
// unescaped as a string, and its localized values parsed and unescaped as
|
||||||
|
// localestrings.
|
||||||
|
func (entry Entry) Unescape () (Entry, error) {
|
||||||
|
value, err := ParseString(entry.Value)
|
||||||
|
if err != nil { return Entry { }, err }
|
||||||
|
localizedValue := Entry {
|
||||||
|
Value: value,
|
||||||
|
Localized: make(map[locale.Locale] string),
|
||||||
|
}
|
||||||
|
for name, localized := range entry.Localized {
|
||||||
|
unescaped, err := ParseLocaleString(localized)
|
||||||
|
if err != nil { return Entry { }, err }
|
||||||
|
localizedValue.Localized[name] = unescaped
|
||||||
|
}
|
||||||
|
return localizedValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ParseString parses a value of type string.
|
// ParseString parses a value of type string.
|
||||||
// Values of type string may contain all ASCII characters except for control
|
// Values of type string may contain all ASCII characters except for control
|
||||||
@@ -269,6 +300,8 @@ func ParseString (value string) (string, error) {
|
|||||||
// The escape sequences \s, \n, \t, \r, and \\ are supported, meaning ASCII
|
// The escape sequences \s, \n, \t, \r, and \\ are supported, meaning ASCII
|
||||||
// space, newline, tab, carriage return, and backslash, respectively.
|
// space, newline, tab, carriage return, and backslash, respectively.
|
||||||
func ParseLocaleString (value string) (string, error) {
|
func ParseLocaleString (value string) (string, error) {
|
||||||
|
value, err := escapeString(value)
|
||||||
|
if err != nil { return "", err }
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +327,15 @@ func ParseBoolean (value string) (bool, error) {
|
|||||||
return false, ErrBooleanNotTrueOrFalse
|
return false, ErrBooleanNotTrueOrFalse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseInteger parses a value of type integer.
|
||||||
|
// The geniuses at freedesktop never explained this type at all, or how it
|
||||||
|
// should be parsed.
|
||||||
|
func ParseInteger (value string) (int, error) {
|
||||||
|
// TODO ensure this is compliant
|
||||||
|
integer, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
return int(integer), err
|
||||||
|
}
|
||||||
|
|
||||||
// ParseNumeric parses a value of type numeric.
|
// ParseNumeric parses a value of type numeric.
|
||||||
// Values of type numeric must be a valid floating point number as recognized by
|
// Values of type numeric must be a valid floating point number as recognized by
|
||||||
// the %f specifier for scanf in the C locale.
|
// the %f specifier for scanf in the C locale.
|
||||||
@@ -303,11 +345,11 @@ func ParseNumeric (value string) (float64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseMultiple parses multiple of a value type. Any value parsing function can
|
// ParseMultiple parses multiple of a value type. Any value parsing function can
|
||||||
// be specified. The multiple values should be separated by a semicolon and the
|
// be specified. The multiple values should be separated by a separator and the
|
||||||
// input string may be optionally terminated by a semicolon. Trailing empty
|
// input string may be optionally terminated by a separator. Trailing empty
|
||||||
// strings must always be terminated with a semicolon. Semicolons in these
|
// strings must always be terminated with a separator. Separators in these
|
||||||
// values need to be escaped using \;.
|
// values need to be escaped using \<sep>.
|
||||||
func ParseMultiple[T any] (parser func (string) (T, error), value string) ([]T, error) {
|
func ParseMultiple[T any] (parser func (string) (T, error), value string, sep rune) ([]T, error) {
|
||||||
values := []T { }
|
values := []T { }
|
||||||
builder := strings.Builder { }
|
builder := strings.Builder { }
|
||||||
|
|
||||||
@@ -324,9 +366,9 @@ func ParseMultiple[T any] (parser func (string) (T, error), value string) ([]T,
|
|||||||
switch char {
|
switch char {
|
||||||
case '\\':
|
case '\\':
|
||||||
backslash = true
|
backslash = true
|
||||||
case ';':
|
case sep:
|
||||||
if backslash {
|
if backslash {
|
||||||
builder.WriteRune(';')
|
builder.WriteRune(sep)
|
||||||
backslash = false
|
backslash = false
|
||||||
} else {
|
} else {
|
||||||
err := newValue()
|
err := newValue()
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ func testParseString (test *testing.T, input string, correct File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testParseErr (test *testing.T, input string, correct error) {
|
||||||
|
_, err := Parse(strings.NewReader(input))
|
||||||
|
if err != correct {
|
||||||
|
test.Fatalf (
|
||||||
|
"errors are different\n---correct---\n%v\n---got---\n%v\n",
|
||||||
|
err, correct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testEntry (value string) Entry {
|
func testEntry (value string) Entry {
|
||||||
entry := newEntry()
|
entry := newEntry()
|
||||||
entry.Value = value
|
entry.Value = value
|
||||||
@@ -168,3 +177,66 @@ File {
|
|||||||
"Icon": testEntry("fooview-new"),
|
"Icon": testEntry("fooview-new"),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
func TestParseLocalized (test *testing.T) {
|
||||||
|
testParseString(test, `
|
||||||
|
[Group 0]
|
||||||
|
Name=something
|
||||||
|
Name[xx_XX]=other thing
|
||||||
|
Name[yy_YY]=another thing
|
||||||
|
Name[zz_ZZ]=yet another thing
|
||||||
|
[Group 1]
|
||||||
|
Name[xx_XX]=other thing
|
||||||
|
Name[yy_YY]=another thing
|
||||||
|
Name=something
|
||||||
|
Name[zz_ZZ]=yet another thing
|
||||||
|
`,
|
||||||
|
File {
|
||||||
|
"Group 0": Group {
|
||||||
|
"Name": Entry {
|
||||||
|
Value: "something",
|
||||||
|
Localized: map[locale.Locale] string {
|
||||||
|
locale.Locale {
|
||||||
|
Lang: "xx",
|
||||||
|
Country: "XX",
|
||||||
|
}: "other thing",
|
||||||
|
locale.Locale {
|
||||||
|
Lang: "yy",
|
||||||
|
Country: "YY",
|
||||||
|
}: "another thing",
|
||||||
|
locale.Locale {
|
||||||
|
Lang: "zz",
|
||||||
|
Country: "ZZ",
|
||||||
|
}: "yet another thing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Group 1": Group {
|
||||||
|
"Name": Entry {
|
||||||
|
Value: "something",
|
||||||
|
Localized: map[locale.Locale] string {
|
||||||
|
locale.Locale {
|
||||||
|
Lang: "xx",
|
||||||
|
Country: "XX",
|
||||||
|
}: "other thing",
|
||||||
|
locale.Locale {
|
||||||
|
Lang: "yy",
|
||||||
|
Country: "YY",
|
||||||
|
}: "another thing",
|
||||||
|
locale.Locale {
|
||||||
|
Lang: "zz",
|
||||||
|
Country: "ZZ",
|
||||||
|
}: "yet another thing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
|
||||||
|
func TestParseErrNoDefaultValue (test *testing.T) {
|
||||||
|
testParseErr(test, `
|
||||||
|
[Group 0]
|
||||||
|
Name[xx_XX]=other thing
|
||||||
|
Name[yy_YY]=another thing
|
||||||
|
Name[zz_ZZ]=yet another thing
|
||||||
|
`, ErrNoDefaultValue)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user