shortcuts.service.ts 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. import fs from 'fs'
  2. import os from 'os'
  3. import path from 'path'
  4. import type { DesktopApplication } from '@shared/types'
  5. function applicationDirs(): string[] {
  6. const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share')
  7. const dataDirs = (process.env.XDG_DATA_DIRS || '/usr/local/share:/usr/share').split(':').filter(Boolean)
  8. return [dataHome, ...dataDirs].map((dir) => path.join(dir, 'applications'))
  9. }
  10. function readDesktopEntry(file: string): Record<string, string> {
  11. const entry: Record<string, string> = {}
  12. let inEntry = false
  13. for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
  14. if (line.startsWith('[')) {
  15. inEntry = line.trim() === '[Desktop Entry]'
  16. continue
  17. }
  18. const separator = line.indexOf('=')
  19. if (!inEntry || separator < 0 || line.slice(0, separator).includes('[')) continue
  20. const key = line.slice(0, separator).trim()
  21. if (!(key in entry)) entry[key] = line.slice(separator + 1).trim()
  22. }
  23. return entry
  24. }
  25. /**
  26. * Apps the user could pick as a wallpaper shortcut, the way a launcher menu lists them: an id earlier in the
  27. * XDG data dirs hides the same id further down, hidden and NoDisplay entries are left out.
  28. */
  29. export function listDesktopApplications(): DesktopApplication[] {
  30. const seen = new Set<string>()
  31. const result: DesktopApplication[] = []
  32. for (const dir of applicationDirs()) {
  33. let files: string[]
  34. try {
  35. files = fs.readdirSync(dir, { recursive: true }) as string[]
  36. } catch {
  37. continue
  38. }
  39. for (const relative of files) {
  40. if (!relative.endsWith('.desktop')) continue
  41. const id = relative.split(path.sep).join('-')
  42. if (seen.has(id)) continue
  43. seen.add(id)
  44. try {
  45. const entry = readDesktopEntry(path.join(dir, relative))
  46. if (entry.Type !== 'Application' || entry.NoDisplay === 'true' || entry.Hidden === 'true' || !entry.Name) continue
  47. result.push({ name: entry.Name, file: path.join(dir, relative) })
  48. } catch {
  49. // unreadable entries are skipped
  50. }
  51. }
  52. }
  53. return result.sort((a, b) => a.name.localeCompare(b.name))
  54. }