- Shortcut: Cmd+Shift+C → Cmd+Shift+M (avoid Chrome DevTools conflict) - Preload: onData/onExit now return cleanup functions (fix memory leak) - PTY: resolve shell path at construction time, throw on missing claude CLI - Terminal: remove allowProposedApi, dedupe resize listeners, add cleanup - Main: debounce window move config writes (500ms), restart PTY on activate - CSS: remove unused orbGlow keyframes - Package: fix appId to com.apaam.myclaude, skip code signing, remove unused deps
199 lines
4.9 KiB
TypeScript
199 lines
4.9 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
|
|
let moveTimeout: NodeJS.Timeout | 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
|
|
|
|
const x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20
|
|
const 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,
|
|
},
|
|
})
|
|
|
|
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
|
|
})
|
|
|
|
mainWindow.on('moved', () => {
|
|
if (!mainWindow) return
|
|
if (moveTimeout) clearTimeout(moveTimeout)
|
|
moveTimeout = setTimeout(() => {
|
|
if (!mainWindow) return
|
|
const [x, y] = mainWindow.getPosition()
|
|
const config = loadConfig()
|
|
saveConfig({ ...config, windowPosition: { x, y } })
|
|
}, 500)
|
|
})
|
|
}
|
|
|
|
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]
|
|
|
|
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] } })
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
createWindow()
|
|
ptyManager.start('claude')
|
|
|
|
mainWindow?.webContents.on('did-finish-load', () => {
|
|
collapseWindow()
|
|
})
|
|
|
|
const ret = globalShortcut.register('CommandOrControl+Shift+M', () => {
|
|
if (!mainWindow) {
|
|
createWindow()
|
|
return
|
|
}
|
|
if (mainWindow.isVisible()) {
|
|
mainWindow.hide()
|
|
} else {
|
|
mainWindow.show()
|
|
mainWindow.focus()
|
|
}
|
|
})
|
|
|
|
if (!ret) {
|
|
console.log('Global shortcut registration failed')
|
|
}
|
|
|
|
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()
|
|
ptyManager.start('claude')
|
|
}
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|