This commit is contained in:
2026-03-25 22:59:31 +08:00
commit d391deb23b
81 changed files with 13012 additions and 0 deletions

326
src/main/code-launchpad.ts Normal file
View File

@@ -0,0 +1,326 @@
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<checkIDEsResultDto> {
// 构建数据结构的辅助函数
const _ = (command: string, code: JetBrainsProductCode): checkIDEResultDto => {
return {
code,
command,
display: JIN[code],
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<checkIDEsResultDto> {
return global.installedIDEs === null ? await checkIDEs() : global.installedIDEs
}
/**
* 检查系统中的各 IDE 是否安装可用。
* 原理是运行 `where.exe ${command}` 并检查返回值。
*/
export async function checkIDEs(): Promise<checkIDEsResultDto> {
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<checkIDEVersionDto> {
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<checkIDEsVersionDto> {
// 构建数据结构的辅助函数
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(<JetBrainsProductCode>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<checkIDEsVersionDto> {
return global.installedIDEsVersion === null
? await checkIDEsVersion()
: global.installedIDEsVersion
}
/**
* 检查各 IDE 的版本信息。
*/
export async function checkIDEsVersion(): Promise<checkIDEsVersionDto> {
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
}