diff --git a/electron-builder.yml b/electron-builder.yml index 89c99edec2..2fb5a45c71 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -77,6 +77,8 @@ linux: desktop: entry: StartupWMClass: CherryStudio + mimeTypes: + - x-scheme-handler/cherrystudio publish: provider: generic url: https://releases.cherry-ai.com diff --git a/src/main/index.ts b/src/main/index.ts index b1d161ead3..dda49b782e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -8,7 +8,12 @@ import Logger from 'electron-log' import { registerIpc } from './ipc' import { configManager } from './services/ConfigManager' import mcpService from './services/MCPService' -import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient' +import { + CHERRY_STUDIO_PROTOCOL, + handleProtocolUrl, + registerProtocolClient, + setupAppImageDeepLink +} from './services/ProtocolClient' import { registerShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' @@ -53,6 +58,9 @@ if (!app.requestSingleInstanceLock()) { setAppDataDir() + // Setup deep link for AppImage on Linux + await setupAppImageDeepLink() + if (process.env.NODE_ENV === 'development') { installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]) .then((name) => console.log(`Added Extension: ${name}`)) diff --git a/src/main/services/ProtocolClient.ts b/src/main/services/ProtocolClient.ts index 84a8c007c5..1d98f31c92 100644 --- a/src/main/services/ProtocolClient.ts +++ b/src/main/services/ProtocolClient.ts @@ -1,3 +1,11 @@ +import { exec } from 'node:child_process' +import fs from 'node:fs/promises' +import path from 'node:path' +import { promisify } from 'node:util' + +import { app } from 'electron' +import Logger from 'electron-log' + import { handleMcpProtocolUrl } from './urlschema/mcp-install' import { windowService } from './WindowService' @@ -39,3 +47,78 @@ export function handleProtocolUrl(url: string) { }) } } + +const execAsync = promisify(exec) + +const DESKTOP_FILE_NAME = 'cherrystudio-url-handler.desktop' + +/** + * Sets up deep linking for the AppImage build on Linux by creating a .desktop file. + * This allows the OS to open cherrystudio:// URLs with this App. + */ +export async function setupAppImageDeepLink(): Promise { + // Only run on Linux and when packaged as an AppImage + if (process.platform !== 'linux' || !process.env.APPIMAGE) { + return + } + + Logger.info('AppImage environment detected on Linux, setting up deep link.') + + try { + const appPath = app.getPath('exe') + if (!appPath) { + Logger.error('Could not determine App path.') + return + } + + const homeDir = app.getPath('home') + const applicationsDir = path.join(homeDir, '.local', 'share', 'applications') + const desktopFilePath = path.join(applicationsDir, DESKTOP_FILE_NAME) + + // Ensure the applications directory exists + await fs.mkdir(applicationsDir, { recursive: true }) + + // Content of the .desktop file + // %U allows passing the URL to the application + // NoDisplay=true hides it from the regular application menu + const desktopFileContent = `[Desktop Entry] +Name=Cherry Studio +Exec=${escapePathForExec(appPath)} %U +Terminal=false +Type=Application +MimeType=x-scheme-handler/${CHERRY_STUDIO_PROTOCOL}; +NoDisplay=true +` + + // Write the .desktop file (overwrite if exists) + await fs.writeFile(desktopFilePath, desktopFileContent, 'utf-8') + Logger.info(`Created/Updated desktop file: ${desktopFilePath}`) + + // Update the desktop database + // It's important to update the database for the changes to take effect + try { + const { stdout, stderr } = await execAsync(`update-desktop-database ${escapePathForExec(applicationsDir)}`) + if (stderr) { + Logger.warn(`update-desktop-database stderr: ${stderr}`) + } + Logger.info(`update-desktop-database stdout: ${stdout}`) + Logger.info('Desktop database updated successfully.') + } catch (updateError) { + Logger.error('Failed to update desktop database:', updateError) + // Continue even if update fails, as the file is still created. + } + } catch (error) { + // Log the error but don't prevent the app from starting + Logger.error('Failed to setup AppImage deep link:', error) + } +} + +/** + * Escapes a path for safe use within the Exec field of a .desktop file + * and for shell commands. Handles spaces and potentially other special characters + * by quoting. + */ +function escapePathForExec(filePath: string): string { + // Simple quoting for paths with spaces. + return `'${filePath.replace(/'/g, "'\\''")}'` +}