diff --git a/internal/theme/xdgicons/icon.go b/internal/theme/xdgicons/icon.go new file mode 100644 index 0000000..f5966ee --- /dev/null +++ b/internal/theme/xdgicons/icon.go @@ -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 + } +} diff --git a/internal/theme/xdgicons/xdgiconname.go b/internal/theme/xdgicons/xdgiconname.go new file mode 100644 index 0000000..91caf51 --- /dev/null +++ b/internal/theme/xdgicons/xdgiconname.go @@ -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", +}