| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- import { execFile } from 'child_process'
- import { promisify } from 'util'
- import * as os from 'os'
- import * as fs from 'fs'
- import * as path from 'path'
- import { detectDisplayServer, detectDesktopEnv, isCommandAvailable } from '../utils/platform'
- import type { WallpaperBackend, WallpaperEnvironment, WallpaperMeta } from '@shared/types'
- import { getLweStatus, launchLweAsync } from './lwe.service'
- import { getDefaultFps } from './config.service'
- const execFileAsync = promisify(execFile)
- let envCache: WallpaperEnvironment | null = null
- export function invalidateEnvCache(): void {
- envCache = null
- }
- export function detectEnvironment(): WallpaperEnvironment {
- if (envCache) return envCache
- const platform = os.platform()
- const displayServer = detectDisplayServer()
- const desktopEnv = detectDesktopEnv()
- const available: WallpaperBackend[] = []
- if (platform === 'win32') {
- available.push('windows')
- } else if (platform === 'darwin') {
- available.push('macos')
- } else {
- if (isCommandAvailable('swww')) available.push('swww')
- if (isCommandAvailable('swaybg')) available.push('swaybg')
- if (isCommandAvailable('feh')) available.push('feh')
- if (isCommandAvailable('xwallpaper')) available.push('xwallpaper')
- if (desktopEnv === 'gnome') available.push('gsettings')
- if (desktopEnv === 'kde' && isCommandAvailable('qdbus')) available.push('qdbus')
- }
- let recommended: WallpaperBackend = 'auto'
- if (platform === 'win32') recommended = 'windows'
- else if (platform === 'darwin') recommended = 'macos'
- else if (desktopEnv === 'gnome') recommended = 'gsettings'
- else if (desktopEnv === 'kde') recommended = 'qdbus'
- else if (displayServer === 'wayland' && available.includes('swww')) recommended = 'swww'
- else if (available.includes('feh')) recommended = 'feh'
- envCache = {
- displayServer,
- desktopEnv,
- availableBackends: available,
- recommendedBackend: recommended,
- linuxWallpaperEngine: getLweStatus()
- }
- return envCache
- }
- export async function applyWallpaper(imagePath: string, backend?: WallpaperBackend): Promise<void> {
- const platform = os.platform()
- if (platform === 'linux') {
- await applyLinux(imagePath, backend)
- } else if (platform === 'darwin') {
- await applyMacOS(imagePath)
- } else if (platform === 'win32') {
- await applyWindows(imagePath)
- } else {
- throw new Error(`Unsupported platform: ${platform}`)
- }
- }
- async function applyLinux(imagePath: string, preferredBackend?: WallpaperBackend): Promise<void> {
- if (preferredBackend && preferredBackend !== 'auto') {
- await applyWithBackend(preferredBackend, imagePath)
- return
- }
- const backend = pickLinuxBackend()
- if (!backend) {
- throw new Error(
- 'No supported wallpaper backend found. Please install swww (Wayland) or feh (X11).'
- )
- }
- await applyWithBackend(backend, imagePath)
- }
- function pickLinuxBackend(): WallpaperBackend | null {
- const de = detectDesktopEnv()
- if (de === 'gnome') return 'gsettings'
- if (de === 'kde' && isCommandAvailable('qdbus')) return 'qdbus'
- if (detectDisplayServer() === 'wayland') {
- if (isCommandAvailable('swww')) return 'swww'
- if (isCommandAvailable('swaybg')) return 'swaybg'
- }
- if (isCommandAvailable('feh')) return 'feh'
- if (isCommandAvailable('xwallpaper')) return 'xwallpaper'
- return null
- }
- async function applyWithBackend(backend: WallpaperBackend, imagePath: string): Promise<void> {
- switch (backend) {
- case 'swww':
- await execFileAsync('swww', ['img', imagePath, '--transition-type', 'fade'])
- break
- case 'swaybg':
- await execFileAsync('swaybg', ['-i', imagePath, '-m', 'fill'])
- break
- case 'feh':
- await execFileAsync('feh', ['--bg-fill', imagePath])
- break
- case 'xwallpaper':
- await execFileAsync('xwallpaper', ['--zoom', imagePath])
- break
- case 'gsettings':
- await execFileAsync('gsettings', [
- 'set',
- 'org.gnome.desktop.background',
- 'picture-uri',
- `file://${imagePath}`
- ])
- break
- case 'qdbus': {
- const script = `
- var allDesktops = desktops();
- for (var i = 0; i < allDesktops.length; i++) {
- var d = allDesktops[i];
- d.wallpaperPlugin = 'org.kde.image';
- d.currentConfigGroup = ['Wallpaper', 'org.kde.image', 'General'];
- d.writeConfig('Image', 'file://${imagePath}');
- }
- `
- await execFileAsync('qdbus', [
- 'org.kde.plasmashell',
- '/PlasmaShell',
- 'org.kde.PlasmaShell.evaluateScript',
- script
- ])
- break
- }
- case 'windows':
- await applyWindows(imagePath)
- break
- case 'macos':
- await applyMacOS(imagePath)
- break
- default:
- throw new Error(`Unknown backend: ${backend}`)
- }
- }
- async function applyMacOS(imagePath: string): Promise<void> {
- const script = `tell application "Finder" to set desktop picture to POSIX file "${imagePath}"`
- await execFileAsync('osascript', ['-e', script])
- }
- async function applyWindows(imagePath: string): Promise<void> {
- // Pass path via environment variable to avoid injection risk from special characters
- const script = `
- Add-Type -TypeDefinition @'
- using System.Runtime.InteropServices;
- public class Wallpaper {
- [DllImport("user32.dll", CharSet=CharSet.Auto)]
- public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
- }
- '@
- [Wallpaper]::SystemParametersInfo(20, 0, $env:WE_WALLPAPER_PATH, 3)
- `
- await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
- env: { ...process.env, WE_WALLPAPER_PATH: imagePath }
- })
- }
- function findWallpaperImage(dirPath: string): string {
- if (!fs.existsSync(dirPath)) {
- throw new Error(`Directory not found: ${dirPath}`)
- }
- if (!fs.statSync(dirPath).isDirectory()) {
- return dirPath
- }
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
- const files = fs.readdirSync(dirPath)
- const preview = files.find((f) => f.toLowerCase().startsWith('preview'))
- if (preview && imageExtensions.includes(path.extname(preview).toLowerCase())) {
- return path.join(dirPath, preview)
- }
- const image = files.find((f) => imageExtensions.includes(path.extname(f).toLowerCase()))
- if (image) return path.join(dirPath, image)
- throw new Error(`No image found in ${dirPath}`)
- }
- /**
- * Apply a library wallpaper: renders animated/scene/web/video wallpapers via
- * linux-wallpaperengine when available, otherwise falls back to a static image.
- * Shared by the manual "apply" IPC handler and the playlist playback engine.
- */
- export async function applyWallpaperMeta(
- wallpaper: WallpaperMeta,
- options: { fps?: number; volume?: number; backend?: WallpaperBackend } = {}
- ): Promise<string> {
- if (!wallpaper.localPath) throw new Error(`Wallpaper ${wallpaper.id} has no local path`)
- const isAnimated = wallpaper.type === 'scene' || wallpaper.type === 'web' || wallpaper.type === 'video'
- const lwe = getLweStatus()
- if (isAnimated && os.platform() === 'linux' && lwe.installed) {
- const fps = options.fps ?? wallpaper.fpsOverride ?? getDefaultFps() ?? undefined
- await launchLweAsync(wallpaper.localPath, { fps, volume: options.volume })
- return wallpaper.localPath
- }
- // Static fallback: set the preview image as a plain wallpaper
- const appliedPath = findWallpaperImage(wallpaper.localPath)
- await applyWallpaper(appliedPath, options.backend)
- return appliedPath
- }
|