diff --git a/backend/src/ipc/apps/linux.ts b/backend/src/ipc/apps/linux.ts index b5490cb..08a3c93 100644 --- a/backend/src/ipc/apps/linux.ts +++ b/backend/src/ipc/apps/linux.ts @@ -1,48 +1,73 @@ import fs from "fs"; import fsPromises from "fs/promises"; import path from "path"; +import os from "os"; import { exec } from "child_process"; import util from "util"; -import os from "os"; const execAsync = util.promisify(exec); -async function parseDesktopFile(filePath: string): Promise<{ name: string; icon: string; path: string } | null> { +interface DesktopEntry { + Name?: string; + Icon?: string; + Exec?: string; + NoDisplay?: string; + Hidden?: string; +} + +async function parseDesktopFile(filePath: string): Promise { try { const content = await fsPromises.readFile(filePath, "utf-8"); const lines = content.split("\n"); - let name = ""; - let icon = ""; - let execPath = ""; + const entry: DesktopEntry = {}; + let inDesktopEntry = false; for (const line of lines) { - if (line.startsWith("Name=")) { - name = line.split("=")[1]; - } else if (line.startsWith("Icon=")) { - icon = line.split("=")[1]; - } else if (line.startsWith("Exec=")) { - execPath = line.split("=")[1].split(" ")[0]; + if (line.trim() === "[Desktop Entry]") { + inDesktopEntry = true; + continue; } - } + if (line.trim().startsWith("[") && line.trim().endsWith("]")) { + inDesktopEntry = false; + continue; + } + if (!inDesktopEntry) continue; - if (name && execPath) { - return { name, icon, path: execPath }; + const [key, ...valueParts] = line.split("="); + if (key && valueParts.length > 0) { + entry[key.trim() as keyof DesktopEntry] = valueParts.join("=").trim(); + } } + + return entry; } catch (error) { console.error(`Error parsing desktop file ${filePath}:`, error); + return null; } - - return null; } -async function getIconPath(iconName: string): Promise { - try { - const { stdout } = await execAsync(`find /usr/share/icons -name "${iconName}.*" | head -n 1`); - return stdout.trim(); - } catch (error) { - console.error(`Error finding icon for ${iconName}:`, error); - return ""; +async function findIconPath(iconName: string): Promise { + const iconDirs = [ + "/usr/share/icons", + "/usr/share/pixmaps", + path.join(os.homedir(), ".icons"), + path.join(os.homedir(), ".local/share/icons"), + ]; + + for (const dir of iconDirs) { + try { + const { stdout } = await execAsync(`find "${dir}" -name "${iconName}.*" | head -n 1`); + const iconPath = stdout.trim(); + if (iconPath) { + return iconPath; + } + } catch (error) { + // Ignore errors and continue searching + } } + + // If no icon found, return the original name + return iconName; } async function getLinuxApplicationsInDirectory(dir: string): Promise<{ name: string; icon: string; path: string }[]> { @@ -57,10 +82,15 @@ async function getLinuxApplicationsInDirectory(dir: string): Promise<{ name: str for (const file of files) { if (file.endsWith(".desktop")) { const filePath = path.join(dir, file); - const app = await parseDesktopFile(filePath); - if (app) { - const iconPath = await getIconPath(app.icon); - apps.push({ ...app, icon: iconPath }); + const entry = await parseDesktopFile(filePath); + if (entry && entry.Name && entry.Exec && entry.NoDisplay !== "true" && entry.Hidden !== "true") { + const execCommand = entry.Exec.split(" ").filter(part => !part.startsWith("%")); + const iconPath = entry.Icon ? await findIconPath(entry.Icon) : ""; + apps.push({ + name: entry.Name, + icon: iconPath, + path: execCommand.join(" ") + }); } } } @@ -72,10 +102,17 @@ async function getLinuxApplicationsInDirectory(dir: string): Promise<{ name: str } export async function getLinuxApplications(): Promise<{ name: string; icon: string; path: string }[]> { + const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"); + const extraDataDirs = process.env.XDG_DATA_DIRS + ? process.env.XDG_DATA_DIRS.split(":") + : ["/usr/local/share", "/usr/share", "/var/lib/flatpak/exports/share"]; + + const flatpakDir = path.join(dataHome, "flatpak", "exports", "share"); + const directories = [ - "/usr/share/applications", - "/usr/local/share/applications", - path.join(os.homedir(), ".local/share/applications") + path.join(dataHome, "applications"), + path.join(flatpakDir, "applications"), + ...extraDataDirs.map(dir => path.join(dir, "applications")) ]; const applications: { name: string; icon: string; path: string }[] = []; @@ -88,4 +125,4 @@ export async function getLinuxApplications(): Promise<{ name: string; icon: stri await Promise.all(promises); return applications; -} +} \ No newline at end of file diff --git a/backend/src/ipc/open/app.ts b/backend/src/ipc/open/app.ts index e3679a7..a3c2f60 100644 --- a/backend/src/ipc/open/app.ts +++ b/backend/src/ipc/open/app.ts @@ -1,5 +1,28 @@ import { IpcMainInvokeEvent, shell } from "electron"; +import { exec } from "child_process"; +import util from "util"; -export async function openApp(event: IpcMainInvokeEvent, appPath: string) { - return shell.openPath(appPath); -} +const execAsync = util.promisify(exec); + +export async function openApp(event: IpcMainInvokeEvent, appPath: string): Promise { + if (process.platform === "linux") { + try { + // Split the command and its arguments + const [command, ...args] = appPath.split(' '); + + // Escape each argument individually + const escapedArgs = args.map(arg => `"${arg.replace(/"/g, '\\"')}"`); + + // Reconstruct the command + const fullCommand = `${command} ${escapedArgs.join(' ')}`; + + await execAsync(fullCommand); + return "App launched successfully"; + } catch (error) { + console.error(`Error launching app: ${error}`); + return `Failed to launch app: ${error}`; + } + } else { + return shell.openPath(appPath); + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1a9d924..0aeb831 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "quick-finder", - "version": "0.1.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-popover": "^1.1.2", @@ -27,6 +26,7 @@ "react-dom": "19.0.0-rc-66855b96-20241106", "react-error-boundary": "^4.1.2", "react-markdown": "^9.0.1", + "react-rewards": "^2.0.4", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7" }, @@ -5901,6 +5901,16 @@ } } }, + "node_modules/react-rewards": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-rewards/-/react-rewards-2.0.4.tgz", + "integrity": "sha512-Lw7gIhD8yPDzC6boaVmcXwuTHRLSLAdqB3kZc+29YWvdHWsuc3fdAZlxI8Cm8fvD8fhP+3JkZBtzX224czw15w==", + "license": "MIT", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -11132,6 +11142,11 @@ "tslib": "^2.0.0" } }, + "react-rewards": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-rewards/-/react-rewards-2.0.4.tgz", + "integrity": "sha512-Lw7gIhD8yPDzC6boaVmcXwuTHRLSLAdqB3kZc+29YWvdHWsuc3fdAZlxI8Cm8fvD8fhP+3JkZBtzX224czw15w==" + }, "react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", diff --git a/package.json b/package.json index 4329dd8..a9362da 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.14", + "version": "0.0.15", "scripts": { "start": "cd backend && npm run start", "code": "code ./frontend && code ./backend",