import { execFile } from "node:child_process"; import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; import { app, BrowserWindow, desktopCapturer, ipcMain, session, shell, } from "electron"; import { checkForUpdates } from "./updates.js"; import { ipcChannels, type DesktopCallStatus, type DesktopScreenShareAudioOutput, type DesktopScreenShareCapabilities, } from "../shared/ipc.js"; import { initTray, setTrayCallStatus } from "./tray.js"; import { clearSecureStorage, deleteSecureStorage, getSecureStorageStatus, loadSecureStorage, saveSecureStorage, } from "./secureStorage.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const verbose = process.argv.includes("--verbose"); let mainWindow: BrowserWindow | null = null; let selectedScreenShareSourceId: string | null = null; app.setName("tensamin"); app.setPath("userData", join(app.getPath("appData"), "tensamin", "electron")); if (verbose) { app.commandLine.appendSwitch("enable-logging", "stderr"); app.commandLine.appendSwitch("v", "1"); app.commandLine.appendSwitch("log-level", "0"); } if ( process.platform === "linux" && !app.commandLine.hasSwitch("password-store") ) { app.commandLine.appendSwitch("password-store", "gnome-libsecret"); } if ( process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland" && !process.env.TENSAMIN_ENABLE_VULKAN ) { app.commandLine.appendSwitch("disable-features", "Vulkan"); } function verboseLog(...args: unknown[]) { if (verbose) { console.log("[tensamin:electron]", ...args); } } function getRendererIndex() { if (!app.isPackaged) { return resolve(__dirname, "../../../web/dist/index.html"); } return join(process.resourcesPath, "web", "index.html"); } function getWindowIcon() { if (process.platform === "darwin") return undefined; if (app.isPackaged) return join(process.resourcesPath, "icons", "icon.png"); return resolve(__dirname, "../../build/icons/icon.png"); } function getPlatform(): DesktopScreenShareCapabilities["platform"] { if (process.platform === "linux") return "linux"; if (process.platform === "darwin") return "macos"; if (process.platform === "win32") return "windows"; return "other"; } function getScreenShareCapabilities(): DesktopScreenShareCapabilities { const platform = getPlatform(); return { runtime: "electron", platform, showAudioOutputSelector: platform === "linux", showAudioSwitch: platform === "windows" || platform === "macos", hasReliableSystemAudio: platform === "windows", }; } function execJson(command: string, args: string[]) { verboseLog("exec", command, args.join(" ")); return new Promise((resolvePromise, reject) => { execFile(command, args, { timeout: 3000 }, (error, stdout, stderr) => { if (error) { reject(new Error(stderr.trim() || error.message)); return; } resolvePromise(JSON.parse(stdout)); }); }); } async function listAudioOutputs(): Promise { verboseLog("listAudioOutputs", { platform: process.platform }); if (process.platform !== "linux") return []; const sinks = await execJson("pactl", ["--format=json", "list", "sinks"]); if (!Array.isArray(sinks)) return []; return sinks .map((sink) => { if (!sink || typeof sink !== "object") return null; const record = sink as Record; const id = record.index == null ? undefined : String(record.index); const name = typeof record.description === "string" ? record.description : id; if (!id || !name) return null; return { id, name, isDefault: false }; }) .filter( (output): output is DesktopScreenShareAudioOutput => output != null, ); } async function listScreenShareSources() { verboseLog("listScreenShareSources"); const sources = await desktopCapturer.getSources({ types: ["screen", "window"], thumbnailSize: { width: 320, height: 180 }, fetchWindowIcons: true, }); return sources.map((source) => ({ id: source.id, kind: source.id.startsWith("screen:") ? "screen" : "window", name: source.name, subtitle: source.id, thumbnail: source.thumbnail.isEmpty() ? null : source.thumbnail.toDataURL(), })); } function registerDisplayMediaHandler() { session.defaultSession.setDisplayMediaRequestHandler( async (_request, callback) => { verboseLog("display media request", { selectedScreenShareSourceId }); const sources = await desktopCapturer.getSources({ types: ["screen", "window"], thumbnailSize: { width: 0, height: 0 }, }); const selected = sources.find( (source) => source.id === selectedScreenShareSourceId, ); selectedScreenShareSourceId = null; const video = selected ?? sources[0]; if (!video) { callback({}); return; } if (process.platform === "win32") { callback({ video, audio: "loopback" }); return; } callback({ video }); }, ); } function registerMediaPermissionHandler() { const isTrustedRenderer = (url: string) => { try { const parsed = new URL(url); return parsed.protocol === "file:"; } catch { return false; } }; session.defaultSession.setPermissionCheckHandler( (_webContents, permission, requestingOrigin) => permission === "media" && isTrustedRenderer(requestingOrigin), ); session.defaultSession.setPermissionRequestHandler( (_webContents, permission, callback, details) => { callback( permission === "media" && isTrustedRenderer(details.requestingUrl), ); }, ); } function registerIpc() { verboseLog("registering ipc handlers"); ipcMain.handle(ipcChannels.listScreenShareSources, listScreenShareSources); ipcMain.handle(ipcChannels.listScreenShareAudioOutputs, listAudioOutputs); ipcMain.handle( ipcChannels.getScreenShareCapabilities, getScreenShareCapabilities, ); ipcMain.handle( ipcChannels.selectScreenShareSource, (_event, sourceId: unknown) => { if ( typeof sourceId !== "string" || sourceId.length === 0 || sourceId.length > 256 ) { throw new Error("Invalid screen share source id."); } selectedScreenShareSourceId = sourceId; verboseLog("selected screen share source", sourceId); return true; }, ); ipcMain.handle(ipcChannels.getVersion, () => app.getVersion()); ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates); ipcMain.handle(ipcChannels.getSecureStorageStatus, getSecureStorageStatus); ipcMain.handle(ipcChannels.loadSecureStorage, (_event, key: unknown) => loadSecureStorage(key), ); ipcMain.handle( ipcChannels.saveSecureStorage, (_event, key: unknown, value: unknown) => saveSecureStorage(key, value), ); ipcMain.handle(ipcChannels.deleteSecureStorage, (_event, key: unknown) => deleteSecureStorage(key), ); ipcMain.handle(ipcChannels.clearSecureStorage, clearSecureStorage); ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => { if ( typeof status !== "object" || status === null || typeof (status as DesktopCallStatus).inCall !== "boolean" || typeof (status as DesktopCallStatus).speaking !== "boolean" || ((status as DesktopCallStatus).iconDataUrl !== undefined && (typeof (status as DesktopCallStatus).iconDataUrl !== "string" || !(status as DesktopCallStatus).iconDataUrl?.startsWith( "data:image/png;base64,", ) || (status as DesktopCallStatus).iconDataUrl!.length > 16_384)) ) { throw new Error("Invalid call status."); } const { inCall, iconDataUrl } = status as DesktopCallStatus; setTrayCallStatus(inCall, iconDataUrl); }); ipcMain.handle(ipcChannels.minimizeWindow, () => { verboseLog("window:minimize"); mainWindow?.minimize(); }); ipcMain.handle(ipcChannels.maximizeWindow, () => { verboseLog("window:maximize"); if (!mainWindow) return; if (mainWindow.isMaximized()) { mainWindow.unmaximize(); return; } mainWindow.maximize(); }); ipcMain.handle(ipcChannels.closeWindow, () => { verboseLog("window:close"); mainWindow?.close(); }); } async function createWindow() { const rendererIndex = getRendererIndex(); verboseLog("creating main window", { appVersion: app.getVersion(), electronVersion: process.versions.electron, chromeVersion: process.versions.chrome, nodeVersion: process.versions.node, platform: process.platform, arch: process.arch, isPackaged: app.isPackaged, rendererIndex, argv: process.argv, }); mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 900, minHeight: 600, title: "Tensamin", icon: getWindowIcon(), frame: false, autoHideMenuBar: true, webPreferences: { preload: join(__dirname, "../preload/preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true, webSecurity: true, }, }); mainWindow.setMenuBarVisibility(false); mainWindow.webContents.setWindowOpenHandler(({ url }) => { verboseLog("blocked window open", url); void shell.openExternal(url); return { action: "deny" }; }); if (verbose) { mainWindow.webContents.on( "console-message", (_event, level, message, line, sourceId) => { const target = level >= 2 ? console.error : console.log; target("[tensamin:renderer]", message, { level, line, sourceId }); }, ); mainWindow.webContents.on( "did-fail-load", (_event, errorCode, errorDescription, validatedURL) => { console.error("[tensamin:electron] renderer failed to load", { errorCode, errorDescription, validatedURL, }); }, ); mainWindow.webContents.on("did-finish-load", () => { verboseLog("renderer finished loading", mainWindow?.webContents.getURL()); }); mainWindow.webContents.on("render-process-gone", (_event, details) => { console.error("[tensamin:electron] renderer process gone", details); }); mainWindow.on("unresponsive", () => { console.error("[tensamin:electron] main window became unresponsive"); }); } await mainWindow.loadFile(rendererIndex); } app.on("window-all-closed", () => { verboseLog("window-all-closed"); if (process.platform !== "darwin") app.quit(); }); app.on("activate", () => { verboseLog("activate"); if (BrowserWindow.getAllWindows().length === 0) void createWindow(); }); if (verbose) { process.on("uncaughtException", (error) => { console.error("[tensamin:electron] uncaught exception", error); }); process.on("unhandledRejection", (reason) => { console.error("[tensamin:electron] unhandled rejection", reason); }); } async function start() { verboseLog("waiting for app readiness"); await app.whenReady(); verboseLog("app ready"); registerIpc(); registerDisplayMediaHandler(); registerMediaPermissionHandler(); initTray(() => mainWindow); await createWindow(); } void start().catch((error) => { console.error("[tensamin:electron] failed to start", error); app.exit(1); });