package xdgIcons import "os" import "fmt" import "log" import "image" import "regexp" import "strings" import _ "image/png" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/backend/style" import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme" type iconTheme struct { xdg xdgIconTheme.Theme fallback style.IconSet texturesSmall map[tomo.Icon] canvas.Texture texturesMedium map[tomo.Icon] canvas.Texture texturesLarge map[tomo.Icon] canvas.Texture } func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, error) { this := &iconTheme { fallback: fallback, texturesLarge: make(map[tomo.Icon] canvas.Texture), texturesMedium: make(map[tomo.Icon] canvas.Texture), texturesSmall: make(map[tomo.Icon] canvas.Texture), } xdg, err := xdgIconTheme.FindThemeWarn(name, path...) if err != nil { return nil, err } this.xdg = xdg return this, nil } func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture { switch size { case tomo.IconSizeMedium: return this.texturesMedium case tomo.IconSizeLarge: return this.texturesLarge default: return this.texturesSmall } } func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.Texture, bool) { // TODO use scaling factor instead of 1 // find icon file icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG) if err != nil { return nil, false } // open icon file iconFile, err := os.Open(icon.Path) if err != nil { // this failing indicates a broken icon theme log.Printf("nasin: icon file '%s' is inaccessible: %v\n", icon.Path, err) return nil, false } iconImage, _, err := image.Decode(iconFile) if err != nil { // this failing indicates a broken icon theme log.Printf("nasin: icon file '%s' is broken: %v\n", icon.Path, err) return nil, false } return tomo.NewTexture(iconImage), true } func (this *iconTheme) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture { source := this.selectSource(size) texture, ok := source[icon] if !ok { texture = this.icon(icon, size) source[icon] = texture } if texture == nil { return this.fallback.Icon(icon, size) } else { return texture } } func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture { icon := tomo.Icon(mime.String()) source := this.selectSource(size) texture, ok := source[icon] if !ok { texture = this.mimeIcon(mime, size) source[icon] = texture } if texture == nil { return this.fallback.MimeIcon(mime, size) } else { return texture } } func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture { if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok { return texture } if texture, ok := this.xdgIcon(XdgIconName(generalizeIcon(icon)), size); ok { return texture } return nil } func (this *iconTheme) mimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture { if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok { return texture } if texture, ok := this.xdgIcon(xdgFormatMime(generalizeMimeType(mime)), size); ok { return texture } if texture, ok := this.xdgIcon(xdgFormatMime(data.M("text", "x-generic")), size); ok { return texture } return nil } var kebabMatchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") var kebabMatchAllCaps = regexp.MustCompile("([a-z0-9])([A-Z])") // XdgIconName returns the best XDG name for the given icon. func XdgIconName (icon tomo.Icon) string { if name, ok := xdgIconNames[icon]; ok { return name } name := kebabMatchFirstCap.ReplaceAllString(string(icon), "${1}-${2}") name = kebabMatchAllCaps.ReplaceAllString(string(name), "${1}-${2}") return strings.ToLower(name) } func generalizeIcon (icon tomo.Icon) tomo.Icon { name := string(icon) switch { case strings.HasPrefix(name, "Application"): return tomo.IconApplication case strings.HasPrefix(name, "Preferences"): return tomo.IconPreferences case strings.HasPrefix(name, "Device"): return tomo.IconDevice case strings.HasPrefix(name, "Hardware"): return tomo.IconHardware case strings.HasPrefix(name, "Storage"): return tomo.IconStorageHardDisk case strings.HasPrefix(name, "Input"): return tomo.IconInputMouse case strings.HasPrefix(name, "Network"): return tomo.IconNetworkWired case strings.HasPrefix(name, "Place"): return tomo.IconPlaceDirectory case strings.HasPrefix(name, "Directory"): return tomo.IconPlaceDirectory case strings.HasPrefix(name, "Trash"): return tomo.IconPlaceTrash case strings.HasPrefix(name, "Help"): return tomo.IconHelpContents } switch icon { case tomo.IconCellularSignal0: return tomo.IconWirelessSignal0 case tomo.IconCellularSignal1: return tomo.IconWirelessSignal1 case tomo.IconCellularSignal2: return tomo.IconWirelessSignal2 case tomo.IconCellularSignal3: return tomo.IconWirelessSignal3 } return icon } func xdgFormatMime (mime data.Mime) string { return fmt.Sprintf("%s-%s", mime.Type, mime.Subtype) } func generalizeMimeType (mime data.Mime) data.Mime { // FIXME make this more accurate // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html mime.Subtype = "x-generic" return mime } func iconSizePixels (size tomo.IconSize) int { // TODO: once Tomo has scaling support, take that into account here switch size { case tomo.IconSizeMedium: return 24 case tomo.IconSizeLarge: return 48 default: return 16 } }