- Electron + React + TypeScript 桌面宠物外壳 - xterm.js + node-pty 直接对接 Claude CLI 进程 - 宠物 orb(80x80)可拖拽,点击展开终端面板 - CRT 复古终端主题(扫描线、文字发光) - 全局快捷键 Cmd+Shift+C 显隐 - 窗口位置自动保存 - 中华田园犬吉祥物 icon(AI 生成) - Makefile 新增 `make app` 打包命令
212 lines
5.1 KiB
TypeScript
212 lines
5.1 KiB
TypeScript
import { app, BrowserWindow, ipcMain, screen, globalShortcut } from 'electron'
|
|
import * as path from 'path'
|
|
import { PTYManager } from './pty'
|
|
import { loadConfig, saveConfig } from './config'
|
|
|
|
const ptyManager = new PTYManager()
|
|
let mainWindow: BrowserWindow | null = null
|
|
|
|
const COLLAPSED_SIZE = { width: 80, height: 80 }
|
|
const EXPANDED_SIZE = { width: 900, height: 600 }
|
|
|
|
function createWindow() {
|
|
const config = loadConfig()
|
|
const display = screen.getPrimaryDisplay()
|
|
const { width: screenWidth, height: screenHeight } = display.workAreaSize
|
|
|
|
// Use saved position or default to bottom-right
|
|
let x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20
|
|
let y = config.windowPosition?.y ?? screenHeight - COLLAPSED_SIZE.height - 20
|
|
|
|
mainWindow = new BrowserWindow({
|
|
width: COLLAPSED_SIZE.width,
|
|
height: COLLAPSED_SIZE.height,
|
|
x,
|
|
y,
|
|
frame: false,
|
|
transparent: true,
|
|
backgroundColor: '#00000000',
|
|
alwaysOnTop: true,
|
|
skipTaskbar: true,
|
|
hasShadow: false,
|
|
resizable: false,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
})
|
|
|
|
// Dock hide on macOS
|
|
if (app.dock) {
|
|
app.dock.hide()
|
|
}
|
|
|
|
if (process.env.VITE_DEV_SERVER_URL) {
|
|
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
|
|
} else {
|
|
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
|
|
}
|
|
|
|
mainWindow.on('closed', () => {
|
|
mainWindow = null
|
|
})
|
|
|
|
// Save position when moved
|
|
mainWindow.on('moved', () => {
|
|
if (!mainWindow) return
|
|
const [x, y] = mainWindow.getPosition()
|
|
const config = loadConfig()
|
|
saveConfig({ ...config, windowPosition: { x, y } })
|
|
})
|
|
}
|
|
|
|
function expandWindow() {
|
|
if (!mainWindow) return
|
|
const display = screen.getPrimaryDisplay()
|
|
const { width: screenWidth, height: screenHeight } = display.workAreaSize
|
|
|
|
const currentPos = mainWindow.getPosition()
|
|
let x = currentPos[0]
|
|
let y = currentPos[1]
|
|
|
|
// Ensure window stays on screen
|
|
if (x + EXPANDED_SIZE.width > screenWidth) {
|
|
x = screenWidth - EXPANDED_SIZE.width - 20
|
|
}
|
|
if (y + EXPANDED_SIZE.height > screenHeight) {
|
|
y = screenHeight - EXPANDED_SIZE.height - 20
|
|
}
|
|
|
|
mainWindow.setResizable(true)
|
|
mainWindow.setMinimumSize(EXPANDED_SIZE.width, EXPANDED_SIZE.height)
|
|
mainWindow.setSize(EXPANDED_SIZE.width, EXPANDED_SIZE.height)
|
|
mainWindow.setPosition(x, y)
|
|
mainWindow.setAlwaysOnTop(true)
|
|
mainWindow.setIgnoreMouseEvents(false)
|
|
mainWindow.setHasShadow(true)
|
|
|
|
const config = loadConfig()
|
|
saveConfig({ ...config, collapsed: false })
|
|
}
|
|
|
|
function collapseWindow() {
|
|
if (!mainWindow) return
|
|
|
|
const currentPos = mainWindow.getPosition()
|
|
|
|
mainWindow.setResizable(false)
|
|
mainWindow.setSize(COLLAPSED_SIZE.width, COLLAPSED_SIZE.height)
|
|
mainWindow.setPosition(currentPos[0], currentPos[1])
|
|
mainWindow.setAlwaysOnTop(true)
|
|
mainWindow.setIgnoreMouseEvents(true, { forward: true })
|
|
mainWindow.setHasShadow(false)
|
|
|
|
const config = loadConfig()
|
|
saveConfig({ ...config, collapsed: true, windowPosition: { x: currentPos[0], y: currentPos[1] } })
|
|
}
|
|
|
|
function toggleWindow() {
|
|
if (!mainWindow) {
|
|
createWindow()
|
|
return
|
|
}
|
|
if (mainWindow.isVisible()) {
|
|
mainWindow.hide()
|
|
} else {
|
|
mainWindow.show()
|
|
}
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
createWindow()
|
|
ptyManager.start('claude')
|
|
|
|
// Collapse after load to ensure pet mode
|
|
mainWindow?.webContents.on('did-finish-load', () => {
|
|
collapseWindow()
|
|
})
|
|
|
|
// Register global shortcut
|
|
const ret = globalShortcut.register('CommandOrControl+Shift+C', () => {
|
|
if (!mainWindow) {
|
|
createWindow()
|
|
return
|
|
}
|
|
if (mainWindow.isVisible()) {
|
|
mainWindow.hide()
|
|
} else {
|
|
mainWindow.show()
|
|
mainWindow.focus()
|
|
}
|
|
})
|
|
|
|
if (!ret) {
|
|
console.log('Global shortcut registration failed')
|
|
}
|
|
|
|
// IPC handlers
|
|
ipcMain.on('pty:write', (_event, data: string) => {
|
|
ptyManager.write(data)
|
|
})
|
|
|
|
ipcMain.on('pty:resize', (_event, cols: number, rows: number) => {
|
|
ptyManager.resize(cols, rows)
|
|
})
|
|
|
|
ipcMain.on('window:expand', () => {
|
|
expandWindow()
|
|
})
|
|
|
|
ipcMain.on('window:collapse', () => {
|
|
collapseWindow()
|
|
})
|
|
|
|
ipcMain.on('window:set-position', (_event, x: number, y: number) => {
|
|
mainWindow?.setPosition(x, y)
|
|
})
|
|
|
|
ipcMain.on('window:set-ignore-mouse', (_event, ignore: boolean) => {
|
|
mainWindow?.setIgnoreMouseEvents(ignore, { forward: true })
|
|
})
|
|
|
|
ipcMain.on('window:close', () => {
|
|
mainWindow?.close()
|
|
})
|
|
|
|
ipcMain.on('window:minimize', () => {
|
|
mainWindow?.minimize()
|
|
})
|
|
|
|
ptyManager.on('data', (data: string) => {
|
|
mainWindow?.webContents.send('pty:data', data)
|
|
})
|
|
|
|
ptyManager.on('exit', (code: number) => {
|
|
mainWindow?.webContents.send('pty:exit', code)
|
|
})
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow()
|
|
}
|
|
})
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
ptyManager.kill()
|
|
globalShortcut.unregisterAll()
|
|
if (process.platform !== 'darwin') {
|
|
app.quit()
|
|
}
|
|
})
|
|
|
|
app.on('before-quit', () => {
|
|
ptyManager.kill()
|
|
globalShortcut.unregisterAll()
|
|
})
|
|
|
|
app.on('will-quit', () => {
|
|
globalShortcut.unregisterAll()
|
|
})
|