import type { checkIDEResultDto, checkIDEsResultDto, checkIDEsVersionDto, checkIDEVersionDto } from '@my-type/settings' import { execSync } from 'node:child_process' import { BrowserWindow, screen, shell, Tray } from 'electron' import { is } from '@electron-toolkit/utils' import path from 'path' import { loadJsonFile } from 'load-json-file' import os from 'os' import { JetBrainsIDEDisplayNameEnum as JIN, JetBrainsProductCode, JetBrainsStateDto } from '@my-type/jetbrains-state-tools' import { settingsManager } from './settings' import { codeLaunchpadIcon } from './resources' import { isNodeError } from '@my-type/node-error' import { JetBrainsDataProductDto } from '@my-type/jetbrains-data-products' const JETBRAINS_TOOLBOX_STATE_JSON_PATH = path.join( os.homedir(), 'AppData/Local/JetBrains/Toolbox/state.json' ) /** * 通过 where.exe 检查系统中的指定 IDE 是否安装可用。 * @param display IDE 的正式显示名称,例如 `Visual Studio Code`。 * @param command IDE 的命令行别名,例如 `code`。 * @param code (JetBrains IDE)的产品代号;对于其他 IDE 为空字符串 */ function checkIDE( display: string, command: string, code: JetBrainsProductCode | '' = '' ): checkIDEResultDto | null { try { const paths = execSync(`where.exe ${command}`).toString().split('\n').slice(0, -1) return { code, display: display, command: command, paths: paths } } catch { return null } } /** * 检查 JerBrains IDEs 是否安装可用。 */ async function checkJetBrainsIDEs(): Promise { // 构建数据结构的辅助函数 const _ = (command: string, code: JetBrainsProductCode): checkIDEResultDto => { return { code, command, display: JIN[command], paths: [] } } const result: checkIDEsResultDto = { pycharm: _('pycharm', 'PY'), clion: _('clion', 'CL'), webstorm: _('webstorm', 'WS'), phpstorm: _('phpstorm', 'PS'), idea: _('idea', 'IU') } // 优先从 JBTState.json 查找 if (settingsManager._settings?.codeLaunchpadIDESearchPolicy.includes('JBTState.json')) { // 处理文件不存在,即 JetBrains Toolbox 相关问题 try { const data: JetBrainsStateDto = await loadJsonFile(JETBRAINS_TOOLBOX_STATE_JSON_PATH) for (const ide of data.tools) { for (const resultElement of Object.values(result)) { if (resultElement?.code === ide.productCode) { resultElement.paths = [ide.launchCommand] } } } } catch (error) { if (isNodeError(error)) { if (error.code === 'ENOENT') { console.log('由于 JBTState.json 不存在,回退到 where.exe。') } } else { console.error(error) } } } // 从 where.exe 查找 // 此分支确保在设置中启用 where.exe 方式,以及 JBTState.json 方式没有结果(未找到任何,或出现错误)时进入 if ( settingsManager._settings?.codeLaunchpadIDESearchPolicy.includes('where.exe') && Object.entries(result).length === 0 ) { result['clion'] = checkIDE(JIN.clion, 'clion', 'CL') result['pycharm'] = checkIDE(JIN.pycharm, 'pycharm', 'PY') result['webstorm'] = checkIDE(JIN.webstorm, 'webstorm', 'WS') result['phpstorm'] = checkIDE(JIN.phpstorm, 'phpstorm', 'PS') result['idea'] = checkIDE(JIN.idea, 'idea', 'IU') } return result } /** * 获取系统中的各 IDE 是否安装可用。 */ export async function getIDEs(): Promise { return global.installedIDEs === null ? await checkIDEs() : global.installedIDEs } /** * 检查系统中的各 IDE 是否安装可用。 * 原理是运行 `where.exe ${command}` 并检查返回值。 */ export async function checkIDEs(): Promise { console.log('在系统中查找已安装的 IDE ...') const vscodeIDEs = { vscode: checkIDE('Visual Studio Code', 'code') } global.installedIDEs = { ...vscodeIDEs, ...(await checkJetBrainsIDEs()) } return global.installedIDEs } /** * 检查 VSCode 的版本信息,通过 code --version。 */ async function checkVSCodeVersion(): Promise { const install = execSync('code --version').toString().split('\n')[0] let latest = 'unknown' try { latest = await fetch('https://update.code.visualstudio.com/api/releases/stable') .then((res) => res.json()) .then((data) => data[0]) } catch (error) { console.error('获取 VSCode 版本列表时出现错误。错误提供如下。') console.error(error) } return { code: '', install, latest, display: 'Visual Studio Code' } } /** * 检查 JetBrains IDE 的版本信息 */ export async function checkJetBrainsIDEsVersion(): Promise { // 构建数据结构的辅助函数 const _ = (display: string, code: JetBrainsProductCode): checkIDEVersionDto => { return { code, display, install: 'unknown', latest: 'unknown' } } const result: checkIDEsVersionDto = { pycharm: _('pycharm', 'PY'), clion: _('clion', 'CL'), webstorm: _('webstorm', 'WS'), phpstorm: _('phpstorm', 'PS'), idea: _('idea', 'IU') } // 尝试从 JBTState.json 获取已安装的 JetBrains IDEs 的版本 // TODO:添加更多获取 JetBrains IDEs 已安装版本的方案 try { const data: JetBrainsStateDto = await loadJsonFile(JETBRAINS_TOOLBOX_STATE_JSON_PATH) for (const tool of data.tools) { for (const resultElement of Object.values(result)) { if (resultElement?.code === tool.productCode) { resultElement.install = tool.displayVersion } } } } catch (error) { if (isNodeError(error)) { if (error.code === 'ENOENT') { console.log('由于 JBTState.json 不存在,检查 JetBrains IDEs 已安装版本的任务失败。') } } else { console.error(error) } } // 从命令行获取 JetBrains IDE 的已安装版本的方案仍然在科研中 // 所以这个方法就暂时先结束了~ // 从网络接口获取 JetBrains IDEs 的版本列表 const codes: JetBrainsProductCode[] = [] for (const resultElement of Object.values(result)) { codes.push(resultElement?.code) } try { const params = new URLSearchParams({ type: 'release', code: codes.join(',') }) const data: JetBrainsDataProductDto[] = await fetch( `https://data.services.jetbrains.com/products?${params}` ).then((res) => res.json()) for (const datum of data) { for (const resultElement of Object.values(result)) { if (resultElement?.code === datum.intellijProductCode) { resultElement.latest = datum.releases[0].version } } } } catch (error) { console.error('从 JetBrains IDEs 版本列表接口获取数据失败。错误提供如下。') console.error(error) } return result } /** * 获取各 IDE 的版本信息。 */ export async function getIDEsVersion(): Promise { return global.installedIDEsVersion === null ? await checkIDEsVersion() : global.installedIDEsVersion } /** * 检查各 IDE 的版本信息。 */ export async function checkIDEsVersion(): Promise { let result: checkIDEsVersionDto = {} for (const ide in await getIDEs()) { switch (ide) { case 'vscode': result['vscode'] = await checkVSCodeVersion() break } } result = { ...result, ...(await checkJetBrainsIDEsVersion()) } global.installedIDEsVersion = result return result } /** * 创建代码启动台窗口。 * @return 布尔值,表明创建是否成功 */ export function createCodeLaunchpadWindow(closeOnBlur: boolean): boolean { // 不允许重复创建 if (global.codeLaunchpadWindow !== null) { return false } const windowWidth = settingsManager._settings?.codeLaunchpadWidth ? settingsManager._settings?.codeLaunchpadWidth : 460 const windowHeight = settingsManager._settings?.codeLaunchpadHeight ? settingsManager._settings?.codeLaunchpadHeight : 760 const position = settingsManager._settings?.codeLaunchpadPosition ? settingsManager._settings.codeLaunchpadPosition : 'left top' const codeLaunchpadWindow = new BrowserWindow({ width: windowWidth, height: windowHeight, x: position === 'left top' || position === 'left bottom' ? 0 : screen.getPrimaryDisplay().workArea.width - windowWidth, y: position === 'left top' || position === 'right top' ? 0 : screen.getPrimaryDisplay().workArea.height - windowHeight, frame: false, show: false, autoHideMenuBar: true, // 代码启动台需要置顶 alwaysOnTop: true, resizable: false, backgroundColor: '#1f1f1f', icon: codeLaunchpadIcon, webPreferences: { preload: path.join(__dirname, '../preload/index.mjs'), sandbox: false } }) codeLaunchpadWindow.on('ready-to-show', () => { codeLaunchpadWindow.show() }) // 如有必要,失去焦点时自动关闭 if (closeOnBlur) { codeLaunchpadWindow.on('blur', () => { codeLaunchpadWindow.close() }) } // 关闭代码启动台时 codeLaunchpadWindow.on('close', () => { global.codeLaunchpadWindow = null }) codeLaunchpadWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) // 开发和生产环境的各自设置 if (is.dev && process.env['ELECTRON_RENDERER_URL']) { codeLaunchpadWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/codeLaunchpad/IDEs') } else { codeLaunchpadWindow.loadFile(path.join(__dirname, '../renderer/index.html'), { hash: '/codeLaunchpad/IDEs' }) } global.codeLaunchpadWindow = codeLaunchpadWindow return true } export function createCodeLaunchpadTray(): Tray { const tray = new Tray(codeLaunchpadIcon) tray.on('click', () => createCodeLaunchpadWindow(true)) return tray }