Skip to content

Commit 9c6bc1d

Browse files
committed
shell api migration to electron
1 parent cae954c commit 9c6bc1d

File tree

2 files changed

+179
-3
lines changed

2 files changed

+179
-3
lines changed

src-electron/main-window-ipc.js

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
const { ipcMain, BrowserWindow } = require('electron');
1+
const { ipcMain, BrowserWindow, shell, clipboard } = require('electron');
22
const path = require('path');
3+
const { spawn } = require('child_process');
34

45
const PHOENIX_WINDOW_PREFIX = 'phcode-';
56
const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-';
@@ -20,13 +21,27 @@ function getNextLabel(prefix) {
2021
throw new Error(`No free window label available for prefix: ${prefix}`);
2122
}
2223

24+
// Track close handlers per window
25+
const windowCloseHandlers = new Map();
26+
2327
function registerWindow(win, label) {
2428
windowRegistry.set(label, win);
2529
webContentsToLabel.set(win.webContents.id, label);
2630

2731
win.on('closed', () => {
2832
windowRegistry.delete(label);
2933
webContentsToLabel.delete(win.webContents.id);
34+
windowCloseHandlers.delete(win.webContents.id);
35+
});
36+
}
37+
38+
function setupCloseHandler(win) {
39+
win.on('close', (event) => {
40+
const hasHandler = windowCloseHandlers.get(win.webContents.id);
41+
if (hasHandler && !win.forceClose) {
42+
event.preventDefault();
43+
win.webContents.send('close-requested');
44+
}
3045
});
3146
}
3247

@@ -95,6 +110,138 @@ function registerWindowIpcHandlers() {
95110
win.focus();
96111
}
97112
});
113+
114+
// Get process ID
115+
ipcMain.handle('get-process-id', () => {
116+
return process.pid;
117+
});
118+
119+
// Get platform architecture
120+
ipcMain.handle('get-platform-arch', () => {
121+
return process.arch;
122+
});
123+
124+
// Get current working directory
125+
ipcMain.handle('get-cwd', () => {
126+
return process.cwd();
127+
});
128+
129+
// Fullscreen APIs
130+
ipcMain.handle('is-fullscreen', (event) => {
131+
const win = BrowserWindow.fromWebContents(event.sender);
132+
return win ? win.isFullScreen() : false;
133+
});
134+
135+
ipcMain.handle('set-fullscreen', (event, enable) => {
136+
const win = BrowserWindow.fromWebContents(event.sender);
137+
if (win) {
138+
win.setFullScreen(enable);
139+
}
140+
});
141+
142+
// Window title APIs
143+
ipcMain.handle('set-window-title', (event, title) => {
144+
const win = BrowserWindow.fromWebContents(event.sender);
145+
if (win) {
146+
win.setTitle(title);
147+
}
148+
});
149+
150+
ipcMain.handle('get-window-title', (event) => {
151+
const win = BrowserWindow.fromWebContents(event.sender);
152+
return win ? win.getTitle() : '';
153+
});
154+
155+
// Clipboard APIs
156+
ipcMain.handle('clipboard-read-text', () => {
157+
return clipboard.readText();
158+
});
159+
160+
ipcMain.handle('clipboard-write-text', (event, text) => {
161+
clipboard.writeText(text);
162+
});
163+
164+
// Read file paths from clipboard (platform-specific)
165+
ipcMain.handle('clipboard-read-files', () => {
166+
const formats = clipboard.availableFormats();
167+
168+
// Windows: FileNameW format contains file paths
169+
if (process.platform === 'win32' && formats.includes('FileNameW')) {
170+
const buffer = clipboard.readBuffer('FileNameW');
171+
// FileNameW is null-terminated UTF-16LE string
172+
const paths = buffer.toString('utf16le').split('\0').filter(p => p.length > 0);
173+
return paths;
174+
}
175+
176+
// macOS: public.file-url format
177+
if (process.platform === 'darwin') {
178+
// Try reading as file URLs
179+
const text = clipboard.read('public.file-url');
180+
if (text) {
181+
// Convert file:// URLs to paths
182+
const paths = text.split('\n')
183+
.filter(url => url.startsWith('file://'))
184+
.map(url => decodeURIComponent(url.replace('file://', '')));
185+
if (paths.length > 0) {
186+
return paths;
187+
}
188+
}
189+
}
190+
191+
// Linux: text/uri-list format
192+
if (process.platform === 'linux' && formats.includes('text/uri-list')) {
193+
const text = clipboard.read('text/uri-list');
194+
if (text) {
195+
const paths = text.split('\n')
196+
.filter(url => url.startsWith('file://'))
197+
.map(url => decodeURIComponent(url.replace('file://', '')));
198+
if (paths.length > 0) {
199+
return paths;
200+
}
201+
}
202+
}
203+
204+
return null;
205+
});
206+
207+
// Shell APIs
208+
ipcMain.handle('move-to-trash', async (event, platformPath) => {
209+
await shell.trashItem(platformPath);
210+
});
211+
212+
ipcMain.handle('show-in-folder', (event, platformPath) => {
213+
shell.showItemInFolder(platformPath);
214+
});
215+
216+
ipcMain.handle('open-external', async (event, url) => {
217+
await shell.openExternal(url);
218+
});
219+
220+
// Windows-only: open URL in specific browser (fire and forget)
221+
ipcMain.handle('open-url-in-browser-win', (event, url, browser) => {
222+
if (process.platform !== 'win32') {
223+
throw new Error('open-url-in-browser-win is only supported on Windows');
224+
}
225+
spawn('cmd', ['/c', 'start', browser, url], { shell: true, detached: true, stdio: 'ignore' });
226+
});
227+
228+
// Register close handler for current window
229+
ipcMain.handle('register-close-handler', (event) => {
230+
const win = BrowserWindow.fromWebContents(event.sender);
231+
if (win) {
232+
windowCloseHandlers.set(win.webContents.id, true);
233+
setupCloseHandler(win);
234+
}
235+
});
236+
237+
// Allow close after handler approves
238+
ipcMain.handle('allow-close', (event) => {
239+
const win = BrowserWindow.fromWebContents(event.sender);
240+
if (win) {
241+
win.forceClose = true;
242+
win.close();
243+
}
244+
});
98245
}
99246

100-
module.exports = { registerWindowIpcHandlers, registerWindow, windowRegistry };
247+
module.exports = { registerWindowIpcHandlers, registerWindow, setupCloseHandler, windowRegistry };

src-electron/preload.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,34 @@ contextBridge.exposeInMainWorld('electronAPI', {
9595
createPhoenixWindow: (url, options) => ipcRenderer.invoke('create-phoenix-window', url, options),
9696
closeWindow: () => ipcRenderer.invoke('close-window'),
9797
quitApp: (exitCode) => ipcRenderer.invoke('quit-app', exitCode),
98-
focusWindow: () => ipcRenderer.invoke('focus-window')
98+
focusWindow: () => ipcRenderer.invoke('focus-window'),
99+
100+
// Process and platform info
101+
getProcessId: () => ipcRenderer.invoke('get-process-id'),
102+
getPlatformArch: () => ipcRenderer.invoke('get-platform-arch'),
103+
getCwd: () => ipcRenderer.invoke('get-cwd'),
104+
105+
// Fullscreen APIs
106+
isFullscreen: () => ipcRenderer.invoke('is-fullscreen'),
107+
setFullscreen: (enable) => ipcRenderer.invoke('set-fullscreen', enable),
108+
109+
// Window title APIs
110+
setWindowTitle: (title) => ipcRenderer.invoke('set-window-title', title),
111+
getWindowTitle: () => ipcRenderer.invoke('get-window-title'),
112+
113+
// Clipboard APIs
114+
clipboardReadText: () => ipcRenderer.invoke('clipboard-read-text'),
115+
clipboardWriteText: (text) => ipcRenderer.invoke('clipboard-write-text', text),
116+
clipboardReadFiles: () => ipcRenderer.invoke('clipboard-read-files'),
117+
118+
// Shell APIs
119+
moveToTrash: (platformPath) => ipcRenderer.invoke('move-to-trash', platformPath),
120+
showInFolder: (platformPath) => ipcRenderer.invoke('show-in-folder', platformPath),
121+
openExternal: (url) => ipcRenderer.invoke('open-external', url),
122+
openUrlInBrowserWin: (url, browser) => ipcRenderer.invoke('open-url-in-browser-win', url, browser),
123+
124+
// Close requested handler
125+
onCloseRequested: (callback) => ipcRenderer.on('close-requested', () => callback()),
126+
registerCloseHandler: () => ipcRenderer.invoke('register-close-handler'),
127+
allowClose: () => ipcRenderer.invoke('allow-close')
99128
});

0 commit comments

Comments
 (0)