Files
FanTools/src/main/code-launchpad.ts
2026-03-26 11:18:56 +08:00

327 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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[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<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
}