Add XDG icon theme loader

This commit is contained in:
Sasha Koshka 2024-05-28 21:55:51 -04:00
parent b9163ffe39
commit fc8da2abd5
2 changed files with 303 additions and 0 deletions

View File

@ -0,0 +1,167 @@
package xdgIcons
import "os"
import "fmt"
import "log"
import "image"
import "regexp"
import "strings"
import _ "image/png"
import "git.tebibyte.media/tomo/tomo"
import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/nasin/internal/theme"
type iconTheme struct {
xdg xdgIconTheme.Theme
texturesSmall map[tomo.Icon] canvas.Texture
texturesMedium map[tomo.Icon] canvas.Texture
texturesLarge map[tomo.Icon] canvas.Texture
}
func FindThemeWarn (name string, path ...string) (theme.IconTheme, error) {
this := &iconTheme {
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.Println("nasin: icon file '%s' is inaccessible: %v", icon.Path, err)
return nil, false
}
iconImage, _, err := image.Decode(iconFile)
if err != nil {
// this failing indicates a broken icon theme
log.Println("nasin: icon file '%s' is broken: %v", 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)
if texture, ok := source[icon]; ok { return texture }
texture := this.icon(icon, size)
source[icon] = texture
return texture
}
func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture {
icon := tomo.Icon(mime.String())
source := this.selectSource(size)
if texture, ok := source[icon]; ok { return texture }
texture := this.mimeIcon(mime, size)
source[icon] = texture
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
}
}

View File

@ -0,0 +1,136 @@
package xdgIcons
import "git.tebibyte.media/tomo/tomo"
// icons that can't be directly translated with regex
var xdgIconNames = map[tomo.Icon] string {
tomo.IconUnknown: "image-missing",
tomo.IconFileNew: "document-new",
tomo.IconDirectoryNew: "folder-new",
tomo.IconFileOpen: "document-open",
tomo.IconFileOpenRecent: "document-open-recent",
tomo.IconFilePageSetup: "document-page-setup",
tomo.IconFilePrint: "document-print",
tomo.IconFilePrintPreview: "document-print-preview",
tomo.IconFilePermissions: "document-permissions", // non-standard
tomo.IconFileProperties: "document-properties",
tomo.IconFileRename: "document-rename", // non-standard
tomo.IconFileRevert: "document-revert",
tomo.IconFileSave: "document-save",
tomo.IconFileSaveAs: "document-save-as",
tomo.IconFileSend: "document-send",
tomo.IconFormatAlignCenter: "format-justify-center",
tomo.IconFormatAlignEven: "format-justify-fill",
tomo.IconFormatAlignLeft: "format-justify-left",
tomo.IconFormatAlignRight: "format-justify-right",
tomo.IconMailReceive: "mail-send-receive",
tomo.IconValueIncrement: "list-add",
tomo.IconValueDecrement: "list-remove",
tomo.IconValueReset: "value-reset", // non-standard
tomo.IconApplication: "system-run",
tomo.IconApplicationWebBrowser: "web-browser",
tomo.IconApplicationMesssanger: "internet-messanger", // non-standard
tomo.IconApplicationPhone: "accessories-phone", // non-standard
tomo.IconApplicationMail: "internet-mail-client", // non-standard
tomo.IconApplicationTerminalEmulator: "utilities-terminal",
tomo.IconApplicationFileBrowser: "system-file-manager",
tomo.IconApplicationTextEditor: "accessories-text-editor",
tomo.IconApplicationDocumentViewer: "office-document-viewer", // non-standard
tomo.IconApplicationWordProcessor: "office-word-processor", // non-standard
tomo.IconApplicationSpreadsheet: "office-spreadsheet", // non-standard
tomo.IconApplicationSlideshow: "office-slideshow", // non-standard
tomo.IconApplicationCalculator: "accessories-calculator",
tomo.IconApplicationPreferences: "preferences-system",
tomo.IconApplicationProcessManager: "utilities-system-monitor",
tomo.IconApplicationSystemInformation: "distributor-logo", // non-standard
tomo.IconApplicationManual: "help-browser",
tomo.IconApplicationCamera: "accessories-camera", // non-standard
tomo.IconApplicationImageViewer: "graphics-image-viewer", // non-standard
tomo.IconApplicationMediaPlayer: "audio-video-media-player", // non-standard
tomo.IconApplicationImageEditor: "graphics-image-editor", // non-standard
tomo.IconApplicationAudioEditor: "audio-audio-editor", // non-standard
tomo.IconApplicationVideoEditor: "video-video-editor", // non-standard
tomo.IconApplicationClock: "accessories-clock", // non-standard
tomo.IconApplicationCalendar: "accessories-calendar", // non-standard
tomo.IconApplicationChecklist: "accessories-checklist", // non-standard
tomo.IconApplications: "applications-other",
tomo.IconPreferences: "preferences-other",
tomo.IconPreferencesNetwork: "preferences-system-network",
tomo.IconDevice: "device", // non-standard
tomo.IconDeviceCamera: "camera-photo",
tomo.IconDeviceWebCamera: "camera-web",
tomo.IconDeviceComputer: "computer",
tomo.IconDevicePda: "pda",
tomo.IconDevicePhone: "phone",
tomo.IconDevicePrinter: "printer",
tomo.IconDeviceScanner: "scanner",
tomo.IconDeviceMultimediaPlayer: "multimedia-player",
tomo.IconDeviceVideoDisplay: "video-display",
tomo.IconDeviceAudioInput: "audio-input-microphone",
tomo.IconDeviceAudioOutput: "audio-speakers",
tomo.IconHardware: "card", // non-standard
tomo.IconHardwareCPU: "cpu",
tomo.IconHardwareGPU: "video-card",
tomo.IconHardwareRAM: "ram",
tomo.IconHardwareSoundCard: "audio-card",
tomo.IconHardwareNetworkAdapter: "network-card",
tomo.IconPowerBattery: "battery",
tomo.IconStorageHardDisk: "drive-harddisk",
tomo.IconStorageFloppyDisk: "media-floppy",
tomo.IconStorageSolidState: "drive-solid",
tomo.IconStorageOptical: "media-optical",
tomo.IconStorageFlashStick: "media-removable",
tomo.IconStorageFlashCard: "media-flash",
tomo.IconStorageMagneticTape: "media-tape",
tomo.IconEmblemReadOnly: "emblem-readonly",
tomo.IconPlaceDirectory: "folder",
tomo.IconPlaceRemote: "folder-remote",
tomo.IconPlaceHome: "user-home",
tomo.IconPlaceDownloads: "folder-downloads", // common
tomo.IconPlaceDesktop: "user-desktop",
tomo.IconPlacePhotos: "folder-pictures", // common
tomo.IconPlaceBooks: "folder-books", // non-standard
tomo.IconPlaceBookmarks: "user-bookmarks",
tomo.IconPlaceTrash: "user-trash",
tomo.IconPlaceDocuments: "folder-documents", // common
tomo.IconPlaceRepositories: "folder-repositories", // non-standard
tomo.IconPlaceMusic: "folder-music", // common
tomo.IconPlaceArchives: "folder-archives", // non-standard
tomo.IconPlaceFonts: "folder-fonts", // non-standard
tomo.IconPlaceBinaries: "folder-executables", // non-standard
tomo.IconPlaceVideos: "folder-videos", // common
tomo.IconPlace3DObjects: "folder-3d-objects", // non-standard
tomo.IconPlaceHistory: "folder-history", // non-standard
tomo.IconPlacePreferences: "preferences-other",
tomo.IconDirectoryDragAccept: "folder-drag-accept",
tomo.IconDirectoryFull: "folder-full",
tomo.IconDirectoryOpen: "folder-open",
tomo.IconDirectoryVisiting: "folder-visiting",
tomo.IconTrashFull: "user-trash-full",
tomo.IconResourceLoading: "image-loading",
tomo.IconResourceMissing: "image-missing",
tomo.IconCellularSignal0: "nm-signal-00", // common
tomo.IconCellularSignal1: "nm-signal-25", // common
tomo.IconCellularSignal2: "nm-signal-75", // common
tomo.IconCellularSignal3: "nm-signal-100", // common
tomo.IconWirelessSignal0: "wifi-signal-00", // common
tomo.IconWirelessSignal1: "wifi-signal-25", // common
tomo.IconWirelessSignal2: "wifi-signal-75", // common
tomo.IconWirelessSignal3: "wifi-signal-100", // common
tomo.IconPrintError: "printer-error",
tomo.IconPrintPrinting: "printer-printing",
tomo.IconBattery0: "battery-caution",
tomo.IconBattery1: "battery-low",
tomo.IconBattery2: "battery-good", // common
tomo.IconBattery3: "battery-full", // common
tomo.IconBrightness0: "brightness-dim", // non-standard
tomo.IconBrightness1: "brightness-medium", // non-standard
tomo.IconBrightness2: "brightness-bright", // non-standard
tomo.IconBrightness3: "brightness-full", // non-standard
tomo.IconVolume0: "audio-volume-muted",
tomo.IconVolume1: "audio-volume-low",
tomo.IconVolume2: "audio-volume-medium",
tomo.IconVolume3: "audio-volume-high",
tomo.IconPlaylistRepeat: "media-playlist-repeat",
tomo.IconPlaylistShuffle: "media-playlist-shuffle",
}