Files
markdown-converter/src/renderer/lib/ipc.ts
T
amitwh cf6b6817b9 fix: wire orphaned IPC channels, fix data-loss bug, print preview events, sidebar navigation
- Fix file.clearRecent sending IPC to main process instead of clearing openTabs
- Wire show-batch-converter, show-document-compare, open-header-footer-dialog IPC channels
- Add print preview event listeners (mc:print-preview, mc:print-preview-styled) in App.tsx
- Fix sidebar hidden bridge buttons toggling sidebar closed after scroll
- Update sidebar menu labels to match actual sections (Files, Outline, Git)
- Fix ipc.print to be an object with show/doPrint methods (was bare function)
- Update callers: ExportPdfDialog, pdf-export, PrintPreview
- Add 7 regression tests for all fixes
- Clean 2 obsolete snapshots
- Build Linux packages (deb, AppImage, snap) — 549 tests passing
2026-06-11 07:15:33 +05:30

146 lines
5.7 KiB
TypeScript

import type {
IpcResult,
FileResult,
FileEntry,
PdfOptions,
DocxOptions,
HtmlOptions,
ExportResult,
BatchItem,
BatchOptions,
BatchResult,
} from '@/types/ipc';
type ChannelMissing = { code: 'CHANNEL_MISSING'; message: string };
function wrap<T>(fn: () => Promise<T>): Promise<IpcResult<T | ChannelMissing>> {
if (typeof window === 'undefined' || !window.electronAPI) {
return Promise.resolve({
ok: false,
error: { code: 'NO_BRIDGE', message: 'window.electronAPI is unavailable' },
});
}
return fn().then(
(data) => ({ ok: true as const, data }),
(err: Error) => ({
ok: false as const,
error: { code: err.name || 'IPC_ERROR', message: err.message || String(err) },
}),
);
}
function safeCall<T extends (...args: any[]) => Promise<any>>(
channel: string,
method: string,
...args: Parameters<T>
): Promise<IpcResult<Awaited<ReturnType<T>> | ChannelMissing>> {
if (typeof window === 'undefined' || !window.electronAPI) {
return Promise.resolve({
ok: false,
error: { code: 'NO_BRIDGE', message: 'window.electronAPI is unavailable' },
});
}
const target = (window.electronAPI as any)[channel]?.[method];
if (!target) {
return Promise.resolve({
ok: false,
error: { code: 'CHANNEL_MISSING', message: `Missing channel: ${channel}.${method}` },
});
}
if (typeof target !== 'function') {
return Promise.resolve({
ok: false,
error: { code: 'CHANNEL_MISSING', message: `Not a function: ${channel}.${method}` },
});
}
return wrap(() => target(...args));
}
export const ipc = {
file: {
open: (): Promise<IpcResult<FileResult | ChannelMissing>> =>
safeCall('file', 'open'),
read: (path: string): Promise<IpcResult<string | ChannelMissing>> =>
safeCall('file', 'read', path),
write: (path: string, content: string): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('file', 'write', path, content),
list: (dir: string): Promise<IpcResult<FileEntry[] | ChannelMissing>> =>
safeCall('file', 'list', dir),
pickFolder: (): Promise<IpcResult<string | null | ChannelMissing>> =>
safeCall('file', 'pickFolder'),
pickFile: (): Promise<IpcResult<string | null | ChannelMissing>> =>
safeCall('file', 'pickFile'),
onChange: (cb: (path: string) => void): (() => void) => {
if (typeof window === 'undefined' || !window.electronAPI?.file?.onChange) {
return () => {};
}
return window.electronAPI.file.onChange(cb);
},
search: (args: { rootPath: string; query: string; isRegex: boolean; caseSensitive: boolean }): Promise<IpcResult<Array<{ filePath: string; line: number; content: string }> | ChannelMissing>> =>
safeCall('file', 'search', args),
gitStatus: (args: { rootPath: string }): Promise<IpcResult<Array<{ filePath: string; status: 'modified' | 'added' | 'deleted' | 'untracked' }> | ChannelMissing>> =>
safeCall('file', 'gitStatus', args),
writeBuffer: (args: { path: string; buffer: Uint8Array }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('file', 'writeBuffer', args),
},
print: {
show: (args: { html: string }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('print', 'show', args),
doPrint: (args: { withStyles?: boolean }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('print', 'doPrint', args),
},
export: {
pdf: (opts: PdfOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
safeCall('export', 'pdf', opts),
docx: (opts: DocxOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
safeCall('export', 'docx', opts),
html: (opts: HtmlOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
safeCall('export', 'html', opts),
batch: (items: BatchItem[], opts: BatchOptions): Promise<IpcResult<BatchResult | ChannelMissing>> =>
safeCall('export', 'batch', items, opts),
},
app: {
getVersion: (): Promise<IpcResult<string | ChannelMissing>> =>
safeCall('app', 'getVersion'),
openExternal: (url: string): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('app', 'openExternal', url),
showItemInFolder: (path: string): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('app', 'showItemInFolder', path),
showSaveDialog: (args?: { title?: string; defaultPath?: string }): Promise<IpcResult<string | null | ChannelMissing>> =>
safeCall('app', 'showSaveDialog', args),
},
menu: {
/**
* Subscribe to a native-menu IPC channel. The callback receives the
* raw payload(s) from the main process. Returns an unsubscribe fn.
*/
on: (channel: string, callback: (...args: unknown[]) => void): (() => void) => {
if (typeof window === 'undefined' || !window.electronAPI?.on) {
return () => {};
}
return window.electronAPI.on(channel, callback as (...a: any[]) => void);
},
},
updater: {
check: (): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('updater', 'check'),
install: (): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('updater', 'install'),
getState: (): Promise<IpcResult<unknown | ChannelMissing>> =>
safeCall('updater', 'getState'),
onStatus: (cb: (payload: unknown) => void): (() => void) => {
if (typeof window === 'undefined' || !window.electronAPI?.updater?.onStatus) {
return () => {};
}
return window.electronAPI.updater.onStatus(cb);
},
},
crash: {
read: (): Promise<IpcResult<unknown | ChannelMissing>> =>
safeCall('crash', 'read'),
openDir: (): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('crash', 'openDir'),
delete: (filename: string): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('crash', 'delete', filename),
},
};