소스 검색

wallpapers open now

UwU 7 달 전
부모
커밋
4888462135
5개의 변경된 파일과 178개의 추가작업 그리고 16개의 파일을 삭제
  1. 7 1
      src/main/ipc/lwe.handlers.ts
  2. 132 11
      src/main/services/lwe.service.ts
  3. 2 0
      src/preload/index.ts
  4. 36 4
      src/renderer/src/components/settings/SettingsView.tsx
  5. 1 0
      src/shared/ipc-channels.ts

+ 7 - 1
src/main/ipc/lwe.handlers.ts

@@ -1,6 +1,6 @@
 import { ipcMain, type BrowserWindow } from 'electron'
 import { IpcChannels } from '@shared/ipc-channels'
-import { getLweStatus, detectDistro, installLweDeps, installLwe, launchLwe, stopLwe, isLweRunning } from '../services/lwe.service'
+import { getLweStatus, detectDistro, installLweDeps, installLwe, uninstallLwe, launchLwe, stopLwe, isLweRunning } from '../services/lwe.service'
 import { invalidateEnvCache } from '../services/wallpaper.service'
 
 export function registerLweHandlers(win: BrowserWindow): void {
@@ -23,6 +23,12 @@ export function registerLweHandlers(win: BrowserWindow): void {
     return getLweStatus()
   })
 
+  ipcMain.handle(IpcChannels.LWE_UNINSTALL, async () => {
+    const result = await uninstallLwe()
+    invalidateEnvCache()
+    return result
+  })
+
   ipcMain.handle(IpcChannels.LWE_LAUNCH, (_e, wallpaperPath: string, options?: { screenRoot?: string; fps?: number }) => {
     launchLwe(wallpaperPath, options)
     return { ok: true, running: isLweRunning() }

+ 132 - 11
src/main/services/lwe.service.ts

@@ -7,6 +7,7 @@ import type { BrowserWindow } from 'electron'
 import { IpcChannels } from '@shared/ipc-channels'
 import type { LweStatus, LweInstallProgress, LinuxDistro } from '@shared/types'
 import { isCommandAvailable, invalidateCommandCache, whichCommand } from '../utils/platform'
+import { getWorkshopPath } from '../utils/paths'
 
 const execFileAsync = promisify(execFile)
 
@@ -14,16 +15,48 @@ const LWE_BINARY = 'linux-wallpaperengine'
 const LWE_REPO = 'https://github.com/Almamu/linux-wallpaperengine.git'
 const BUILD_DIR = path.join(os.tmpdir(), 'lwe-build')
 
+/** Common paths where linux-wallpaperengine may be installed outside $PATH. */
+const LWE_SEARCH_PATHS = [
+  '/usr/local/bin/linux-wallpaperengine',
+  '/usr/local/linux-wallpaperengine',
+  '/usr/bin/linux-wallpaperengine',
+  path.join(os.homedir(), '.local', 'bin', 'linux-wallpaperengine'),
+  path.join(os.homedir(), 'bin', 'linux-wallpaperengine'),
+  '/opt/linux-wallpaperengine/linux-wallpaperengine'
+]
+
 let activeProcess: ChildProcess | null = null
 
+/** Find the LWE binary — first via $PATH, then by scanning common install locations. */
+function findLweBinary(): string | undefined {
+  // 1. Check $PATH
+  const inPath = whichCommand(LWE_BINARY)
+  if (inPath) return inPath
+
+  // 2. Scan common locations
+  for (const p of LWE_SEARCH_PATHS) {
+    try {
+      fs.accessSync(p, fs.constants.X_OK)
+      return p
+    } catch { /* not here */ }
+  }
+
+  return undefined
+}
+
 export function getLweStatus(): LweStatus {
-  const installed = isCommandAvailable(LWE_BINARY)
-  if (!installed) return { installed: false }
+  // Try $PATH first (fast, cached)
+  if (isCommandAvailable(LWE_BINARY)) {
+    return { installed: true, path: whichCommand(LWE_BINARY) }
+  }
 
-  return {
-    installed: true,
-    path: whichCommand(LWE_BINARY)
+  // Scan common paths for existing installs not on $PATH
+  const found = findLweBinary()
+  if (found) {
+    return { installed: true, path: found }
   }
+
+  return { installed: false }
 }
 
 export function detectDistro(): LinuxDistro {
@@ -205,7 +238,7 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
     // pkexec often runs the command in a different cwd (e.g. / or root's home), so run install
     // inside a shell that cd's to the build dir first; PATH preserved for make
     const pathEnv = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin'
-    const installScript = `cd ${JSON.stringify(cmakeBuild)} && export PATH=${JSON.stringify(pathEnv)} && make install`
+    const installScript = `cd ${JSON.stringify(cmakeBuild)} && export PATH=${JSON.stringify(pathEnv)} && make install && ldconfig`
     await execFileAsync(installTool, ['bash', '-c', installScript], {
       timeout: 120_000,
       maxBuffer: 2 * 1024 * 1024,
@@ -288,9 +321,82 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
   }
 }
 
+export async function uninstallLwe(): Promise<{ ok: boolean; message: string }> {
+  const status = getLweStatus()
+  if (!status.installed || !status.path) {
+    return { ok: false, message: 'linux-wallpaperengine is not installed.' }
+  }
+
+  // Stop any running instance first
+  stopLwe()
+
+  const elevate = isCommandAvailable('pkexec') ? 'pkexec' : 'sudo'
+  const binaryPath = status.path
+
+  // Resolve symlink to find the real binary location
+  let realPath: string
+  try {
+    realPath = fs.realpathSync(binaryPath)
+  } catch {
+    realPath = binaryPath
+  }
+
+  // Collect all files to remove (binary, symlink if different, known lib dir)
+  const filesToRemove: string[] = [realPath]
+  if (realPath !== binaryPath) filesToRemove.push(binaryPath)
+
+  // Check for LWE lib directory next to the real binary (cmake install puts libs there)
+  const binDir = path.dirname(realPath)
+  const libDir = path.join(binDir, 'lib')
+  if (fs.existsSync(libDir)) {
+    // Only remove if it looks like it belongs to LWE (contains libcef or similar)
+    try {
+      const entries = fs.readdirSync(libDir)
+      if (entries.some(e => e.includes('cef') || e.includes('wallpaper'))) {
+        filesToRemove.push(libDir)
+      }
+    } catch { /* ignore */ }
+  }
+
+  try {
+    await execFileAsync(elevate, ['rm', '-rf', ...filesToRemove], {
+      timeout: 30_000,
+      encoding: 'utf8'
+    })
+
+    // Invalidate caches
+    invalidateCommandCache(LWE_BINARY)
+
+    return { ok: true, message: 'linux-wallpaperengine has been uninstalled.' }
+  } catch (err) {
+    return { ok: false, message: `Uninstall failed: ${(err as Error).message}` }
+  }
+}
+
+/** Find the Wallpaper Engine assets directory from the Steam install. */
+function findWeAssetsDir(): string | undefined {
+  const workshopPath = getWorkshopPath()
+  // Workshop path looks like: .../steamapps/workshop/content/431960
+  // Assets are at:            .../steamapps/common/wallpaper_engine/assets
+  const steamappsIdx = workshopPath.lastIndexOf(path.join('steamapps', 'workshop'))
+  if (steamappsIdx !== -1) {
+    const steamappsRoot = workshopPath.substring(0, steamappsIdx + 'steamapps'.length)
+    const assetsDir = path.join(steamappsRoot, 'common', 'wallpaper_engine', 'assets')
+    if (fs.existsSync(assetsDir)) return assetsDir
+  }
+
+  // Fallback: scan common Steam locations
+  const home = os.homedir()
+  const candidates = [
+    path.join(home, '.steam', 'steam', 'steamapps', 'common', 'wallpaper_engine', 'assets'),
+    path.join(home, '.local', 'share', 'Steam', 'steamapps', 'common', 'wallpaper_engine', 'assets')
+  ]
+  return candidates.find(p => fs.existsSync(p))
+}
+
 /** Directory containing the LWE binary; used for LD_LIBRARY_PATH. */
 function getLweLibDir(): string {
-  const p = whichCommand(LWE_BINARY)
+  const p = findLweBinary()
   if (!p) return '/usr/local'
   try {
     const resolved = fs.realpathSync(p)
@@ -300,10 +406,21 @@ function getLweLibDir(): string {
   }
 }
 
-/** LD_LIBRARY_PATH value so the loader finds LWE's .so files (binary dir + lib subdir). */
+/** LD_LIBRARY_PATH value so the loader finds LWE's .so files. */
 function getLweLdLibraryPath(): string {
-  const libDir = getLweLibDir()
-  const paths = [libDir, path.join(libDir, 'lib')]
+  const binDir = getLweLibDir()
+  // Cover all possible lib locations: binary dir itself, lib/, lib64/, and the /usr/local standard paths
+  const candidates = [
+    binDir,
+    path.join(binDir, 'lib'),
+    path.join(binDir, 'lib64'),
+    '/usr/local/lib',
+    '/usr/local/lib64'
+  ]
+  // Only include paths that actually exist
+  const paths = candidates.filter(p => {
+    try { return fs.statSync(p).isDirectory() } catch { return false }
+  })
   const existing = process.env.LD_LIBRARY_PATH
   if (existing) paths.push(existing)
   return paths.join(':')
@@ -311,7 +428,7 @@ function getLweLdLibraryPath(): string {
 
 /** Full path to LWE binary (resolved symlink). */
 function getLweBinaryPath(): string {
-  const p = whichCommand(LWE_BINARY)
+  const p = findLweBinary()
   if (!p) return LWE_BINARY
   try {
     return fs.realpathSync(p)
@@ -328,6 +445,8 @@ export function launchLweAsync(
   stopLwe()
 
   const args: string[] = []
+  const assetsDir = findWeAssetsDir()
+  if (assetsDir) args.push('--assets-dir', assetsDir)
   if (options.screenRoot) args.push('--screen-root', options.screenRoot)
   if (options.fps) args.push('--fps', String(options.fps))
   args.push(wallpaperPath)
@@ -394,6 +513,8 @@ export function launchLwe(
 ): ChildProcess {
   stopLwe()
   const args: string[] = []
+  const assetsDir = findWeAssetsDir()
+  if (assetsDir) args.push('--assets-dir', assetsDir)
   if (options.screenRoot) args.push('--screen-root', options.screenRoot)
   if (options.fps) args.push('--fps', String(options.fps))
   args.push(wallpaperPath)

+ 2 - 0
src/preload/index.ts

@@ -113,6 +113,8 @@ const api = {
       ipcRenderer.invoke(IpcChannels.LWE_INSTALL_DEPS),
     install: (): Promise<LweStatus> =>
       ipcRenderer.invoke(IpcChannels.LWE_INSTALL),
+    uninstall: (): Promise<{ ok: boolean; message: string }> =>
+      ipcRenderer.invoke(IpcChannels.LWE_UNINSTALL),
     launch: (wallpaperPath: string, options?: { screenRoot?: string; fps?: number }): Promise<{ ok: boolean; running: boolean }> =>
       ipcRenderer.invoke(IpcChannels.LWE_LAUNCH, wallpaperPath, options),
     stop: (): Promise<{ ok: boolean }> =>

+ 36 - 4
src/renderer/src/components/settings/SettingsView.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect } from 'react'
-import { FolderOpen, Save, Upload, Download, CheckCircle, XCircle, Loader2, Package } from 'lucide-react'
+import { FolderOpen, Save, Upload, Download, CheckCircle, XCircle, Loader2, Package, Trash2 } from 'lucide-react'
 import type { LweStatus, LweInstallProgress, LinuxDistro } from '../../../../shared/types'
 
 const DISTRO_LABELS: Record<LinuxDistro, string> = {
@@ -20,6 +20,8 @@ export default function SettingsView() {
   const [lweProgress, setLweProgress] = useState<LweInstallProgress | null>(null)
   const [distro, setDistro] = useState<LinuxDistro | null>(null)
   const [depsInstalling, setDepsInstalling] = useState(false)
+  const [uninstalling, setUninstalling] = useState(false)
+  const [uninstallMsg, setUninstallMsg] = useState<string | null>(null)
 
   useEffect(() => {
     window.electronAPI.config.get().then((cfg) => {
@@ -100,7 +102,22 @@ export default function SettingsView() {
     }
   }
 
-  const isBusy = lweInstalling || depsInstalling
+  async function handleUninstallLwe() {
+    if (!confirm('Are you sure you want to uninstall linux-wallpaperengine?')) return
+    setUninstalling(true)
+    setUninstallMsg(null)
+    try {
+      const result = await window.electronAPI.lwe.uninstall()
+      setUninstallMsg(result.message)
+      window.electronAPI.lwe.status().then(setLweStatus)
+    } catch (err) {
+      setUninstallMsg(`Uninstall failed: ${(err as Error).message}`)
+    } finally {
+      setUninstalling(false)
+    }
+  }
+
+  const isBusy = lweInstalling || depsInstalling || uninstalling
 
   return (
     <div className="p-6 overflow-y-auto h-full">
@@ -178,13 +195,28 @@ export default function SettingsView() {
           {lweStatus === null ? (
             <p className="mt-3 text-xs text-gray-500">Checking...</p>
           ) : lweStatus.installed ? (
-            <div className="mt-3 space-y-1">
+            <div className="mt-3 space-y-3">
               <div className="flex items-center gap-2 text-sm text-green-400">
                 <CheckCircle size={16} />
                 Installed
               </div>
               {lweStatus.path && (
-                <p className="text-xs text-gray-500">{lweStatus.path}</p>
+                <p className="text-xs text-gray-500">Path: {lweStatus.path}</p>
+              )}
+              <button
+                onClick={handleUninstallLwe}
+                disabled={isBusy}
+                className="flex items-center gap-2 rounded-lg bg-red-600/20 px-4 py-2 text-sm text-red-400 hover:bg-red-600/30 disabled:opacity-50"
+              >
+                {uninstalling ? (
+                  <Loader2 size={14} className="animate-spin" />
+                ) : (
+                  <Trash2 size={14} />
+                )}
+                {uninstalling ? 'Uninstalling...' : 'Uninstall'}
+              </button>
+              {uninstallMsg && (
+                <p className="text-xs text-gray-400">{uninstallMsg}</p>
               )}
             </div>
           ) : (

+ 1 - 0
src/shared/ipc-channels.ts

@@ -59,6 +59,7 @@ export const IpcChannels = {
   LWE_DETECT_DISTRO: 'lwe:detect-distro',
   LWE_INSTALL_DEPS: 'lwe:install-deps',
   LWE_INSTALL: 'lwe:install',
+  LWE_UNINSTALL: 'lwe:uninstall',
   LWE_LAUNCH: 'lwe:launch',
   LWE_STOP: 'lwe:stop',