feat(ipc): implement IPC handlers for app control, data, exec, global shortcuts, system, and Jira

This commit is contained in:
Johannes Millan 2025-11-27 13:44:18 +01:00
parent 68e22c0b85
commit 653ae62dbf
7 changed files with 282 additions and 258 deletions

View file

@ -1,28 +1,11 @@
// FRONTEND EVENTS
// ---------------
import {
app,
dialog,
globalShortcut,
ipcMain,
IpcMainEvent,
ProgressBarOptions,
shell,
} from 'electron';
import { IPC } from './shared-with-frontend/ipc-events.const';
import { lockscreen } from './lockscreen';
import { errorHandlerWithFrontendInform } from './error-handler-with-frontend-inform';
import { JiraCfg } from '../src/app/features/issue/providers/jira/jira.model';
import { sendJiraRequest, setupRequestHeadersForImages } from './jira';
import { KeyboardConfig } from '../src/app/features/config/keyboard-config.model';
import { log } from 'electron-log/main';
import { exec } from 'child_process';
import { getWin } from './main-window';
import { quitApp, showOrFocus } from './various-shared';
import { loadSimpleStoreAll, saveSimpleStore } from './simple-store';
import { getIsLocked } from './shared-state';
import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from './backup';
import { pluginNodeExecutor } from './plugin-node-executor';
import { initAppDataIpc } from './ipc-handlers/app-data';
import { initAppControlIpc } from './ipc-handlers/app-control';
import { initSystemIpc } from './ipc-handlers/system';
import { initJiraIpc } from './ipc-handlers/jira';
import { initGlobalShortcutsIpc } from './ipc-handlers/global-shortcuts';
import { initExecIpc } from './ipc-handlers/exec';
export const initIpcInterfaces = (): void => {
// Initialize plugin node executor (registers IPC handlers)
@ -32,240 +15,11 @@ export const initIpcInterfaces = (): void => {
if (!pluginNodeExecutor) {
log('Warning: Plugin node executor failed to initialize');
}
// HANDLER
// -------
ipcMain.handle(IPC.GET_PATH, (ev, name: string) => {
return app.getPath(name as Parameters<typeof app.getPath>[0]);
});
ipcMain.handle(IPC.GET_BACKUP_PATH, () => {
if (process?.windowsStore) {
return BACKUP_DIR_WINSTORE;
} else {
return BACKUP_DIR;
}
});
// BACKEND EVENTS
// --------------
// ...
// ON EVENTS
// ---------
ipcMain.on(IPC.SHUTDOWN_NOW, quitApp);
ipcMain.on(IPC.EXIT, (ev, exitCode: number) => app.exit(exitCode));
ipcMain.on(IPC.RELAUNCH, () => app.relaunch());
ipcMain.on(IPC.OPEN_DEV_TOOLS, () => getWin().webContents.openDevTools());
ipcMain.on(
IPC.SHOW_EMOJI_PANEL,
() => app.isEmojiPanelSupported() && app.showEmojiPanel(),
);
ipcMain.on(IPC.RELOAD_MAIN_WIN, () => getWin().reload());
ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => shell.openPath(path));
ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => shell.openExternal(url));
ipcMain.handle(IPC.SAVE_FILE_DIALOG, async (ev, { filename, data }) => {
const result = await dialog.showSaveDialog(getWin(), {
defaultPath: filename,
filters: [
{ name: 'JSON Files', extensions: ['json'] },
{ name: 'All Files', extensions: ['*'] },
],
});
if (!result.canceled && result.filePath) {
const fs = await import('fs');
await fs.promises.writeFile(result.filePath, data, 'utf-8');
return { success: true, path: result.filePath };
}
return { success: false };
});
ipcMain.handle(IPC.SHARE_NATIVE, async () => {
// Desktop platforms use the share dialog instead of native share
// This allows for more flexibility and better UX with social media options
return { success: false, error: 'Native share not available on desktop' };
});
ipcMain.on(IPC.LOCK_SCREEN, () => {
if (getIsLocked()) {
return;
}
try {
lockscreen();
} catch (e) {
errorHandlerWithFrontendInform(e);
}
});
ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress, progressBarMode }) => {
const mainWin = getWin();
if (mainWin) {
if (progressBarMode === 'none') {
mainWin.setProgressBar(-1);
} else {
mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), {
mode: progressBarMode as ProgressBarOptions['mode'],
});
}
}
});
ipcMain.on(IPC.FLASH_FRAME, (ev) => {
const mainWin = getWin();
if (mainWin) {
mainWin.flashFrame(false);
mainWin.flashFrame(true);
mainWin.once('focus', () => {
mainWin.flashFrame(false);
});
}
});
ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => {
registerShowAppShortCuts(cfg);
});
ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, { jiraCfg }: { jiraCfg: JiraCfg }) => {
setupRequestHeadersForImages(jiraCfg);
});
ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => {
sendJiraRequest(request);
});
ipcMain.on(IPC.SHOW_OR_FOCUS, () => {
const mainWin = getWin();
showOrFocus(mainWin);
});
ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform);
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function registerShowAppShortCuts(cfg: KeyboardConfig): void {
// unregister all previous
globalShortcut.unregisterAll();
const GLOBAL_KEY_CFG_KEYS: (keyof KeyboardConfig)[] = [
'globalShowHide',
'globalToggleTaskStart',
'globalAddNote',
'globalAddTask',
];
if (cfg) {
const mainWin = getWin();
Object.keys(cfg)
.filter((key: string) =>
GLOBAL_KEY_CFG_KEYS.includes(key as keyof KeyboardConfig),
)
.forEach((key: string) => {
let actionFn: () => void;
const shortcut = cfg[key as keyof KeyboardConfig];
switch (key) {
case 'globalShowHide':
actionFn = () => {
if (mainWin.isFocused()) {
// we need to blur the window for windows
mainWin.blur();
mainWin.hide();
} else {
showOrFocus(mainWin);
}
};
break;
case 'globalToggleTaskStart':
actionFn = () => {
mainWin.webContents.send(IPC.TASK_TOGGLE_START);
};
break;
case 'globalAddNote':
actionFn = () => {
showOrFocus(mainWin);
mainWin.webContents.send(IPC.ADD_NOTE);
};
break;
case 'globalAddTask':
actionFn = () => {
showOrFocus(mainWin);
// NOTE: delay slightly to make sure app is ready
mainWin.webContents.send(IPC.SHOW_ADD_TASK_BAR);
};
break;
default:
actionFn = () => undefined;
}
if (shortcut && shortcut.length > 0) {
try {
const ret = globalShortcut.register(shortcut, actionFn) as unknown;
if (!ret) {
errorHandlerWithFrontendInform(
'Global Shortcut registration failed: ' + shortcut,
shortcut,
);
}
} catch (e) {
errorHandlerWithFrontendInform(
'Global Shortcut registration failed: ' + shortcut,
{ e, shortcut },
);
}
}
});
}
}
initAppDataIpc();
initAppControlIpc();
initSystemIpc();
initJiraIpc();
initGlobalShortcutsIpc();
initExecIpc();
};
const COMMAND_MAP_PROP = 'allowedCommands';
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
async function execWithFrontendErrorHandlerInform(
ev: IpcMainEvent,
command: string,
): Promise<void> {
log('trying to run command ' + command);
const existingData = await loadSimpleStoreAll();
const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || [];
if (!Array.isArray(allowedCommands)) {
throw new Error('allowedCommands is no array ???');
}
if (allowedCommands.includes(command)) {
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
}
});
} else {
const mainWin = getWin();
const res = await dialog.showMessageBox(mainWin, {
type: 'question',
buttons: ['Cancel', 'Yes, execute!'],
defaultId: 2,
title: 'Super Productivity Exec',
message:
'Do you want to execute this command? ONLY confirm if you are sure you know what you are doing!!',
detail: command,
checkboxLabel: 'Remember my answer',
checkboxChecked: true,
});
const { response, checkboxChecked } = res;
if (response === 1) {
if (checkboxChecked) {
await saveSimpleStore(COMMAND_MAP_PROP, [...allowedCommands, command]);
}
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
}
});
}
}
}

View file

@ -0,0 +1,57 @@
import { app, ipcMain, ProgressBarOptions } from 'electron';
import { IPC } from '../shared-with-frontend/ipc-events.const';
import { getWin } from '../main-window';
import { quitApp, showOrFocus } from '../various-shared';
import { getIsLocked } from '../shared-state';
import { lockscreen } from '../lockscreen';
import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform';
export const initAppControlIpc = (): void => {
ipcMain.on(IPC.SHUTDOWN_NOW, quitApp);
ipcMain.on(IPC.EXIT, (ev, exitCode: number) => app.exit(exitCode));
ipcMain.on(IPC.RELAUNCH, () => app.relaunch());
ipcMain.on(IPC.OPEN_DEV_TOOLS, () => getWin().webContents.openDevTools());
ipcMain.on(IPC.RELOAD_MAIN_WIN, () => getWin().reload());
ipcMain.on(IPC.SHOW_OR_FOCUS, () => {
const mainWin = getWin();
showOrFocus(mainWin);
});
ipcMain.on(IPC.LOCK_SCREEN, () => {
if (getIsLocked()) {
return;
}
try {
lockscreen();
} catch (e) {
errorHandlerWithFrontendInform(e);
}
});
ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress, progressBarMode }) => {
const mainWin = getWin();
if (mainWin) {
if (progressBarMode === 'none') {
mainWin.setProgressBar(-1);
} else {
mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), {
mode: progressBarMode as ProgressBarOptions['mode'],
});
}
}
});
ipcMain.on(IPC.FLASH_FRAME, (ev) => {
const mainWin = getWin();
if (mainWin) {
mainWin.flashFrame(false);
mainWin.flashFrame(true);
mainWin.once('focus', () => {
mainWin.flashFrame(false);
});
}
});
};

View file

@ -0,0 +1,17 @@
import { app, ipcMain } from 'electron';
import { IPC } from '../shared-with-frontend/ipc-events.const';
import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from '../backup';
export const initAppDataIpc = (): void => {
ipcMain.handle(IPC.GET_PATH, (ev, name: string) => {
return app.getPath(name as Parameters<typeof app.getPath>[0]);
});
ipcMain.handle(IPC.GET_BACKUP_PATH, () => {
if (process?.windowsStore) {
return BACKUP_DIR_WINSTORE;
} else {
return BACKUP_DIR;
}
});
};

View file

@ -0,0 +1,58 @@
import { dialog, ipcMain, IpcMainEvent } from 'electron';
import { IPC } from '../shared-with-frontend/ipc-events.const';
import { exec } from 'child_process';
import { log } from 'electron-log/main';
import { loadSimpleStoreAll, saveSimpleStore } from '../simple-store';
import { getWin } from '../main-window';
import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform';
const COMMAND_MAP_PROP = 'allowedCommands';
export const initExecIpc = (): void => {
ipcMain.on(IPC.EXEC, execWithFrontendErrorHandlerInform);
};
const execWithFrontendErrorHandlerInform = async (
ev: IpcMainEvent,
command: string,
): Promise<void> => {
log('trying to run command ' + command);
const existingData = await loadSimpleStoreAll();
const allowedCommands: string[] = (existingData[COMMAND_MAP_PROP] as string[]) || [];
if (!Array.isArray(allowedCommands)) {
throw new Error('allowedCommands is no array ???');
}
if (allowedCommands.includes(command)) {
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
}
});
} else {
const mainWin = getWin();
const res = await dialog.showMessageBox(mainWin, {
type: 'question',
buttons: ['Cancel', 'Yes, execute!'],
defaultId: 2,
title: 'Super Productivity Exec',
message:
'Do you want to execute this command? ONLY confirm if you are sure you know what you are doing!!',
detail: command,
checkboxLabel: 'Remember my answer',
checkboxChecked: true,
});
const { response, checkboxChecked } = res;
if (response === 1) {
if (checkboxChecked) {
await saveSimpleStore(COMMAND_MAP_PROP, [...allowedCommands, command]);
}
exec(command, (err) => {
if (err) {
errorHandlerWithFrontendInform(err);
}
});
}
}
};

View file

@ -0,0 +1,88 @@
import { globalShortcut, ipcMain } from 'electron';
import { IPC } from '../shared-with-frontend/ipc-events.const';
import { KeyboardConfig } from '../../src/app/features/config/keyboard-config.model';
import { getWin } from '../main-window';
import { showOrFocus } from '../various-shared';
import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform';
export const initGlobalShortcutsIpc = (): void => {
ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => {
registerShowAppShortCuts(cfg);
});
};
const registerShowAppShortCuts = (cfg: KeyboardConfig): void => {
// unregister all previous
globalShortcut.unregisterAll();
const GLOBAL_KEY_CFG_KEYS: (keyof KeyboardConfig)[] = [
'globalShowHide',
'globalToggleTaskStart',
'globalAddNote',
'globalAddTask',
];
if (cfg) {
const mainWin = getWin();
Object.keys(cfg)
.filter((key: string) => GLOBAL_KEY_CFG_KEYS.includes(key as keyof KeyboardConfig))
.forEach((key: string) => {
let actionFn: () => void;
const shortcut = cfg[key as keyof KeyboardConfig];
switch (key) {
case 'globalShowHide':
actionFn = () => {
if (mainWin.isFocused()) {
// we need to blur the window for windows
mainWin.blur();
mainWin.hide();
} else {
showOrFocus(mainWin);
}
};
break;
case 'globalToggleTaskStart':
actionFn = () => {
mainWin.webContents.send(IPC.TASK_TOGGLE_START);
};
break;
case 'globalAddNote':
actionFn = () => {
showOrFocus(mainWin);
mainWin.webContents.send(IPC.ADD_NOTE);
};
break;
case 'globalAddTask':
actionFn = () => {
showOrFocus(mainWin);
// NOTE: delay slightly to make sure app is ready
mainWin.webContents.send(IPC.SHOW_ADD_TASK_BAR);
};
break;
default:
actionFn = () => undefined;
}
if (shortcut && shortcut.length > 0) {
try {
const ret = globalShortcut.register(shortcut, actionFn) as unknown;
if (!ret) {
errorHandlerWithFrontendInform(
'Global Shortcut registration failed: ' + shortcut,
shortcut,
);
}
} catch (e) {
errorHandlerWithFrontendInform(
'Global Shortcut registration failed: ' + shortcut,
{ e, shortcut },
);
}
}
});
}
};

View file

@ -0,0 +1,14 @@
import { ipcMain } from 'electron';
import { IPC } from '../shared-with-frontend/ipc-events.const';
import { JiraCfg } from '../../src/app/features/issue/providers/jira/jira.model';
import { sendJiraRequest, setupRequestHeadersForImages } from '../jira';
export const initJiraIpc = (): void => {
ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, { jiraCfg }: { jiraCfg: JiraCfg }) => {
setupRequestHeadersForImages(jiraCfg);
});
ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => {
sendJiraRequest(request);
});
};

View file

@ -0,0 +1,36 @@
import { app, dialog, ipcMain, shell } from 'electron';
import { IPC } from '../shared-with-frontend/ipc-events.const';
import { getWin } from '../main-window';
export const initSystemIpc = (): void => {
ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => shell.openPath(path));
ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => shell.openExternal(url));
ipcMain.on(
IPC.SHOW_EMOJI_PANEL,
() => app.isEmojiPanelSupported() && app.showEmojiPanel(),
);
ipcMain.handle(IPC.SAVE_FILE_DIALOG, async (ev, { filename, data }) => {
const result = await dialog.showSaveDialog(getWin(), {
defaultPath: filename,
filters: [
{ name: 'JSON Files', extensions: ['json'] },
{ name: 'All Files', extensions: ['*'] },
],
});
if (!result.canceled && result.filePath) {
const fs = await import('fs');
await fs.promises.writeFile(result.filePath, data, 'utf-8');
return { success: true, path: result.filePath };
}
return { success: false };
});
ipcMain.handle(IPC.SHARE_NATIVE, async () => {
// Desktop platforms use the share dialog instead of native share
// This allows for more flexibility and better UX with social media options
return { success: false, error: 'Native share not available on desktop' };
});
};