|
1 | | -import { app, BrowserWindow, globalShortcut, ipcMain, Menu } from 'electron' |
| 1 | +import { app, BrowserWindow, dialog, globalShortcut, ipcMain, Menu, protocol } from 'electron' |
2 | 2 | import { join } from 'path' |
3 | 3 | import { homedir } from 'os' |
4 | 4 | import { readdir, readFile } from 'fs/promises' |
@@ -136,16 +136,45 @@ function createWindow(): void { |
136 | 136 | mainWindow!.show() |
137 | 137 | }) |
138 | 138 |
|
| 139 | + // Relay image drops from preload to renderer |
| 140 | + ipcMain.on('image-dropped-from-renderer', (_event, paths: string[]) => { |
| 141 | + mainWindow?.webContents.send('image-dropped', paths) |
| 142 | + }) |
| 143 | + |
139 | 144 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { |
140 | 145 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) |
141 | 146 | } else { |
142 | 147 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) |
143 | 148 | } |
144 | 149 | } |
145 | 150 |
|
| 151 | +// Register custom protocol to serve local files for image thumbnails |
| 152 | +protocol.registerSchemesAsPrivileged([ |
| 153 | + { scheme: 'local-image', privileges: { bypassCSP: true, supportFetchAPI: true } } |
| 154 | +]) |
| 155 | + |
146 | 156 | app.whenReady().then(() => { |
147 | 157 | electronApp.setAppUserModelId('com.unitmux') |
148 | 158 |
|
| 159 | + // Handle local-image:// requests by serving files from disk |
| 160 | + protocol.handle('local-image', async (request) => { |
| 161 | + const filePath = decodeURIComponent(new URL(request.url).pathname) |
| 162 | + const ext = filePath.split('.').pop()?.toLowerCase() ?? '' |
| 163 | + const mimeMap: Record<string, string> = { |
| 164 | + png: 'image/png', |
| 165 | + jpg: 'image/jpeg', |
| 166 | + jpeg: 'image/jpeg', |
| 167 | + gif: 'image/gif', |
| 168 | + webp: 'image/webp', |
| 169 | + svg: 'image/svg+xml', |
| 170 | + bmp: 'image/bmp' |
| 171 | + } |
| 172 | + const data = await readFile(filePath) |
| 173 | + return new Response(data, { |
| 174 | + headers: { 'Content-Type': mimeMap[ext] ?? 'application/octet-stream' } |
| 175 | + }) |
| 176 | + }) |
| 177 | + |
149 | 178 | // Custom menu: remove Cmd+H (Hide) accelerator to prevent conflict with Ctrl+Cmd+H |
150 | 179 | const menu = Menu.buildFromTemplate([ |
151 | 180 | { |
@@ -187,8 +216,25 @@ app.whenReady().then(() => { |
187 | 216 | } |
188 | 217 | }) |
189 | 218 |
|
190 | | - ipcMain.handle('tmux:send-input', async (_event, { target, text, vimMode }) => { |
191 | | - return sendInput(target, text, vimMode) |
| 219 | + ipcMain.handle('tmux:send-input', async (_event, { target, text, vimMode, images }) => { |
| 220 | + return sendInput(target, text, vimMode, images) |
| 221 | + }) |
| 222 | + |
| 223 | + ipcMain.handle('dialog:open-image', async () => { |
| 224 | + const win = mainWindow |
| 225 | + if (!win) return [] |
| 226 | + // Temporarily disable alwaysOnTop so the native dialog is visible on macOS |
| 227 | + const wasOnTop = win.isAlwaysOnTop() |
| 228 | + if (wasOnTop) win.setAlwaysOnTop(false) |
| 229 | + try { |
| 230 | + const result = await dialog.showOpenDialog(win, { |
| 231 | + properties: ['openFile', 'multiSelections'], |
| 232 | + filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'] }] |
| 233 | + }) |
| 234 | + return result.canceled ? [] : result.filePaths |
| 235 | + } finally { |
| 236 | + if (wasOnTop) win.setAlwaysOnTop(true) |
| 237 | + } |
192 | 238 | }) |
193 | 239 |
|
194 | 240 | ipcMain.handle('tmux:capture-pane', async (_event, target: string) => { |
|
0 commit comments