1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-14 16:22:46 -04:00
Files
DankMaterialShell/core/internal/desktop/mime.go
T
bbedward 018795125e app picker: extend App Picker to integrate with mime overrides
- Adds "DMS Opener" as an option (dms-open.desktop)
- Add mime type GO utils
- Add rememberance to App Picker modal
2026-05-14 13:06:22 -04:00

312 lines
5.9 KiB
Go

package desktop
import (
"bufio"
"bytes"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var (
aliasMap map[string]string
subclassMap map[string][]string
aliasLoaded time.Time
aliasReloadMu sync.Mutex
mimeCacheMap map[string][]string
mimeCacheLoaded time.Time
mimeCacheReloadMu sync.Mutex
)
const aliasTTL = 60 * time.Second
const mimeCacheTTL = 10 * time.Second
func mimeDataDirs() []string {
var dirs []string
seen := make(map[string]bool)
add := func(p string) {
if p == "" || seen[p] {
return
}
seen[p] = true
dirs = append(dirs, p)
}
add(filepath.Join(utils.XDGDataHome(), "mime"))
if env := os.Getenv("XDG_DATA_DIRS"); env != "" {
for d := range strings.SplitSeq(env, ":") {
add(filepath.Join(strings.TrimSpace(d), "mime"))
}
} else {
add("/usr/local/share/mime")
add("/usr/share/mime")
}
return dirs
}
func loadAliasTables() {
aliases := make(map[string]string)
subclasses := make(map[string][]string)
for _, dir := range mimeDataDirs() {
readKV(filepath.Join(dir, "aliases"), func(k, v string) {
if _, ok := aliases[k]; !ok {
aliases[k] = v
}
})
readKV(filepath.Join(dir, "subclasses"), func(k, v string) {
subclasses[k] = append(subclasses[k], v)
})
}
aliasMap = aliases
subclassMap = subclasses
aliasLoaded = time.Now()
}
func readKV(path string, fn func(k, v string)) {
data, err := os.ReadFile(path)
if err != nil {
return
}
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || line[0] == '#' {
continue
}
sp := strings.IndexByte(line, ' ')
if sp <= 0 {
continue
}
fn(strings.TrimSpace(line[:sp]), strings.TrimSpace(line[sp+1:]))
}
}
func ensureAliasTables() {
aliasReloadMu.Lock()
defer aliasReloadMu.Unlock()
if aliasMap == nil || time.Since(aliasLoaded) > aliasTTL {
loadAliasTables()
}
}
func loadMimeCache() {
merged := make(map[string][]string)
seen := make(map[string]map[string]bool)
for _, dir := range applicationDirs() {
data, err := os.ReadFile(filepath.Join(dir, "mimeinfo.cache"))
if err != nil {
continue
}
groups := parseGroups(data)
g := groups["MIME Cache"]
if g == nil {
continue
}
for mime, val := range g.keys {
ids := splitList(val)
if len(ids) == 0 {
continue
}
if seen[mime] == nil {
seen[mime] = make(map[string]bool)
}
for _, id := range ids {
if seen[mime][id] {
continue
}
seen[mime][id] = true
merged[mime] = append(merged[mime], id)
}
}
}
mimeCacheMap = merged
mimeCacheLoaded = time.Now()
}
func ensureMimeCache() {
mimeCacheReloadMu.Lock()
defer mimeCacheReloadMu.Unlock()
if mimeCacheMap == nil || time.Since(mimeCacheLoaded) > mimeCacheTTL {
loadMimeCache()
}
}
func cacheAppsForMime(mimeType string) []string {
ensureMimeCache()
return mimeCacheMap[mimeType]
}
func StripMimeParams(mimeType string) string {
if semi := strings.IndexByte(mimeType, ';'); semi >= 0 {
mimeType = mimeType[:semi]
}
return strings.TrimSpace(mimeType)
}
func canonicalMime(mimeType string) string {
ensureAliasTables()
mimeType = StripMimeParams(mimeType)
if target, ok := aliasMap[mimeType]; ok {
return target
}
return mimeType
}
func mimeChain(mimeType string) []string {
ensureAliasTables()
root := canonicalMime(mimeType)
visited := map[string]bool{root: true}
chain := []string{root}
queue := []string{root}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
for _, parent := range subclassMap[cur] {
if visited[parent] {
continue
}
visited[parent] = true
chain = append(chain, parent)
queue = append(queue, parent)
}
}
return chain
}
func entrySupportsMime(entry *Entry, chain []string) bool {
for _, m := range entry.MimeTypes {
canonical := canonicalMime(m)
if slices.Contains(chain, canonical) || slices.Contains(chain, m) {
return true
}
}
return false
}
func GetDefault(mimeType string) string {
merged := mergedAssociations()
chain := mimeChain(mimeType)
for _, m := range chain {
if id, ok := merged.Defaults[m]; ok {
if !slices.Contains(merged.Removed[m], id) {
return id
}
}
}
for _, m := range chain {
for _, id := range merged.Added[m] {
if !slices.Contains(merged.Removed[m], id) {
return id
}
}
}
for _, m := range chain {
for _, id := range cacheAppsForMime(m) {
if !slices.Contains(merged.Removed[m], id) {
return id
}
}
}
for _, m := range chain {
for _, entry := range AllEntries() {
if entry.Hidden || entry.NoDisplay {
continue
}
if slices.Contains(merged.Removed[m], entry.ID) {
continue
}
if entrySupportsMime(entry, []string{m}) {
return entry.ID
}
}
}
return ""
}
func SetDefault(mimeType, desktopID string) error {
return setDefaultAssociation(mimeType, desktopID)
}
func SetDefaults(mimeTypes []string, desktopID string) error {
return setDefaultAssociations(mimeTypes, desktopID)
}
func AppsForMime(mimeType string) []string {
merged := mergedAssociations()
chain := mimeChain(mimeType)
removed := make(map[string]bool)
for _, m := range chain {
for _, id := range merged.Removed[m] {
removed[id] = true
}
}
seen := make(map[string]bool)
var out []string
add := func(id string) {
if id == "" || removed[id] || seen[id] {
return
}
seen[id] = true
out = append(out, id)
}
for _, m := range chain {
if id := merged.Defaults[m]; id != "" {
add(id)
}
for _, id := range merged.Added[m] {
add(id)
}
}
for _, m := range chain {
for _, id := range cacheAppsForMime(m) {
add(id)
}
}
for _, entry := range AllEntries() {
if entry.Hidden {
continue
}
if entrySupportsMime(entry, chain) {
add(entry.ID)
}
}
return out
}
func QueryDefaults(mimeTypes []string) map[string]string {
out := make(map[string]string, len(mimeTypes))
for _, m := range mimeTypes {
out[m] = GetDefault(m)
}
return out
}