wallpaper.service.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { execFile } from 'child_process'
  2. import { promisify } from 'util'
  3. import * as os from 'os'
  4. import { detectDisplayServer, detectDesktopEnv, isCommandAvailable } from '../utils/platform'
  5. import type { WallpaperBackend, WallpaperEnvironment } from '@shared/types'
  6. import { getLweStatus } from './lwe.service'
  7. const execFileAsync = promisify(execFile)
  8. // Cache environment detection
  9. let envCache: WallpaperEnvironment | null = null
  10. export function invalidateEnvCache(): void {
  11. envCache = null
  12. }
  13. export function detectEnvironment(): WallpaperEnvironment {
  14. if (envCache) return envCache
  15. const platform = os.platform()
  16. const displayServer = detectDisplayServer()
  17. const desktopEnv = detectDesktopEnv()
  18. const available: WallpaperBackend[] = []
  19. if (platform === 'win32') {
  20. available.push('windows')
  21. } else if (platform === 'darwin') {
  22. available.push('macos')
  23. } else {
  24. if (isCommandAvailable('swww')) available.push('swww')
  25. if (isCommandAvailable('swaybg')) available.push('swaybg')
  26. if (isCommandAvailable('feh')) available.push('feh')
  27. if (isCommandAvailable('xwallpaper')) available.push('xwallpaper')
  28. if (desktopEnv === 'gnome') available.push('gsettings')
  29. if (desktopEnv === 'kde' && isCommandAvailable('qdbus')) available.push('qdbus')
  30. }
  31. let recommended: WallpaperBackend = 'auto'
  32. if (platform === 'win32') recommended = 'windows'
  33. else if (platform === 'darwin') recommended = 'macos'
  34. else if (desktopEnv === 'gnome') recommended = 'gsettings'
  35. else if (desktopEnv === 'kde') recommended = 'qdbus'
  36. else if (displayServer === 'wayland' && available.includes('swww')) recommended = 'swww'
  37. else if (available.includes('feh')) recommended = 'feh'
  38. envCache = {
  39. displayServer,
  40. desktopEnv,
  41. availableBackends: available,
  42. recommendedBackend: recommended,
  43. linuxWallpaperEngine: getLweStatus()
  44. }
  45. return envCache
  46. }
  47. export async function applyWallpaper(imagePath: string, backend?: WallpaperBackend): Promise<void> {
  48. const platform = os.platform()
  49. if (platform === 'linux') {
  50. await applyLinux(imagePath, backend)
  51. } else if (platform === 'darwin') {
  52. await applyMacOS(imagePath)
  53. } else if (platform === 'win32') {
  54. await applyWindows(imagePath)
  55. } else {
  56. throw new Error(`Unsupported platform: ${platform}`)
  57. }
  58. }
  59. async function applyLinux(imagePath: string, preferredBackend?: WallpaperBackend): Promise<void> {
  60. const ds = detectDisplayServer()
  61. const de = detectDesktopEnv()
  62. if (preferredBackend && preferredBackend !== 'auto') {
  63. await applyWithBackend(preferredBackend, imagePath)
  64. return
  65. }
  66. if (de === 'gnome') {
  67. await execFileAsync('gsettings', [
  68. 'set',
  69. 'org.gnome.desktop.background',
  70. 'picture-uri',
  71. `file://${imagePath}`
  72. ])
  73. return
  74. }
  75. if (de === 'kde' && isCommandAvailable('qdbus')) {
  76. const script = `
  77. var allDesktops = desktops();
  78. for (var i = 0; i < allDesktops.length; i++) {
  79. var d = allDesktops[i];
  80. d.wallpaperPlugin = 'org.kde.image';
  81. d.currentConfigGroup = ['Wallpaper', 'org.kde.image', 'General'];
  82. d.writeConfig('Image', 'file://${imagePath}');
  83. }
  84. `
  85. await execFileAsync('qdbus', [
  86. 'org.kde.plasmashell',
  87. '/PlasmaShell',
  88. 'org.kde.PlasmaShell.evaluateScript',
  89. script
  90. ])
  91. return
  92. }
  93. if (ds === 'wayland') {
  94. if (isCommandAvailable('swww')) {
  95. await execFileAsync('swww', ['img', imagePath, '--transition-type', 'fade'])
  96. return
  97. }
  98. if (isCommandAvailable('swaybg')) {
  99. await execFileAsync('swaybg', ['-i', imagePath, '-m', 'fill'])
  100. return
  101. }
  102. }
  103. if (isCommandAvailable('feh')) {
  104. await execFileAsync('feh', ['--bg-fill', imagePath])
  105. return
  106. }
  107. if (isCommandAvailable('xwallpaper')) {
  108. await execFileAsync('xwallpaper', ['--zoom', imagePath])
  109. return
  110. }
  111. throw new Error(
  112. 'No supported wallpaper backend found. Please install swww (Wayland) or feh (X11).'
  113. )
  114. }
  115. async function applyWithBackend(backend: WallpaperBackend, imagePath: string): Promise<void> {
  116. switch (backend) {
  117. case 'swww':
  118. await execFileAsync('swww', ['img', imagePath, '--transition-type', 'fade'])
  119. break
  120. case 'swaybg':
  121. await execFileAsync('swaybg', ['-i', imagePath, '-m', 'fill'])
  122. break
  123. case 'feh':
  124. await execFileAsync('feh', ['--bg-fill', imagePath])
  125. break
  126. case 'xwallpaper':
  127. await execFileAsync('xwallpaper', ['--zoom', imagePath])
  128. break
  129. case 'gsettings':
  130. await execFileAsync('gsettings', [
  131. 'set',
  132. 'org.gnome.desktop.background',
  133. 'picture-uri',
  134. `file://${imagePath}`
  135. ])
  136. break
  137. case 'qdbus': {
  138. const script = `
  139. var allDesktops = desktops();
  140. for (var i = 0; i < allDesktops.length; i++) {
  141. var d = allDesktops[i];
  142. d.wallpaperPlugin = 'org.kde.image';
  143. d.currentConfigGroup = ['Wallpaper', 'org.kde.image', 'General'];
  144. d.writeConfig('Image', 'file://${imagePath}');
  145. }
  146. `
  147. await execFileAsync('qdbus', [
  148. 'org.kde.plasmashell',
  149. '/PlasmaShell',
  150. 'org.kde.PlasmaShell.evaluateScript',
  151. script
  152. ])
  153. break
  154. }
  155. case 'windows':
  156. await applyWindows(imagePath)
  157. break
  158. case 'macos':
  159. await applyMacOS(imagePath)
  160. break
  161. default:
  162. throw new Error(`Unknown backend: ${backend}`)
  163. }
  164. }
  165. async function applyMacOS(imagePath: string): Promise<void> {
  166. const script = `tell application "Finder" to set desktop picture to POSIX file "${imagePath}"`
  167. await execFileAsync('osascript', ['-e', script])
  168. }
  169. async function applyWindows(imagePath: string): Promise<void> {
  170. // Pass path via environment variable to avoid injection risk from special characters
  171. const script = `
  172. Add-Type -TypeDefinition @'
  173. using System.Runtime.InteropServices;
  174. public class Wallpaper {
  175. [DllImport("user32.dll", CharSet=CharSet.Auto)]
  176. public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
  177. }
  178. '@
  179. [Wallpaper]::SystemParametersInfo(20, 0, $env:WE_WALLPAPER_PATH, 3)
  180. `
  181. await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
  182. env: { ...process.env, WE_WALLPAPER_PATH: imagePath }
  183. })
  184. }