import ffi from "@inigolabs/ffi-napi"; import { ipcMain, BrowserWindow, BrowserView, app, screen, powerMonitor, Menu, } from "electron"; import util from "util"; import { spawn, exec } from "child_process"; import path from "path"; import parseUrlTemplate from "../utils/urlTemplateHelper"; import createBrowserWindowTemplate from "../utils/createBrowserWindowTemplate"; import openDevToolsWindow from "../utils/openDevToolsWindow"; const execPromise = util.promisify(exec); const isDev = process.env.NODE_ENV === "development"; let wincontrolserver; const kUrl = "openUrl"; const kFile = "openProgram"; const kCloseFile = "closeProgram"; const kToolWindow = "openToolsMenu"; const kFocus = "focus"; const user32 = new ffi.Library("user32", { IsIconic: ["bool", ["long"]], GetLastActivePopup: ["long", ["long"]], SetForegroundWindow: ["bool", ["long"]], ShowWindow: ["bool", ["long", "int"]], }); const WinActivate = (hwnd) => { // use last active to focus modal windows if exists const lastActiveWindow = user32.GetLastActivePopup(hwnd); // use isIconic to detect if win is minimized if (user32.IsIconic(lastActiveWindow)) { user32.ShowWindow(lastActiveWindow, 9); } user32.SetForegroundWindow(lastActiveWindow); }; const startWinControlServer = () => { const logdir = path.join(app.getPath("userData"), "logs"); const wincontrol = spawn("./bin/win-control-server.exe", [ "/f", "taskbarInit", `logdir=${logdir}`, ]); wincontrol.on("close", startWinControlServer); wincontrol.stdin.setEncoding("utf8"); wincontrol.stdout.on("data", (data) => {}); wincontrolserver = wincontrol; }; const stdinWrite = (data) => { const dataString = JSON.stringify(data); wincontrolserver.stdin.write(`${dataString}\n`, (e) => { if (e) { startWinControlServer(); wincontrolserver.stdin.write(`${dataString}\n`); } }); }; export default async function initWindowsChannel(mainWindow) { const slidingToolWindows = new Set(); const browserWindows = new Map(); startWinControlServer(); //* Handle url ipcMain.handle( kUrl, async ( _event, { title, url, fullscreen, frameless, openDefaultBrowser } ) => { if (openDefaultBrowser) { const data = { event: "open-browser", url: parseUrlTemplate(url), frameless, }; stdinWrite(data); return; } if (browserWindows.has(title)) { browserWindows.get(title).focus(); return; } const { width: screenW, height: screenH } = screen.getPrimaryDisplay().workAreaSize; let frame = true; let titleBarStyle = "default"; let w = 1000; let h = 600; let x = Math.floor(screenW / 2 - w / 2); let y = Math.floor(screenH / 2 - h / 2); if (frameless) { titleBarStyle = "hidden"; frame = false; fullscreen = true; } if (fullscreen) { x = 0; y = 0; w = screenW; h = screenH; } let window = new BrowserWindow({ title, minWidth: 600, minHeight: 400, x, y, width: w, height: h, frame, titleBarStyle, center: true, useContentSize: true, webPreferences: { additionalArguments: [title], }, }); const [contentW, contentH] = window.getContentSize(); const view = new BrowserView(); window.setBrowserView(view); if (fullscreen) window.maximize(); view.setBounds({ x: 0, y: 0, width: Math.floor(contentW), height: Math.floor(contentH), }); view.setAutoResize({ width: true, height: true, horizontal: true, vertical: true, }); view.webContents.loadURL(parseUrlTemplate(url)); const menu = Menu.buildFromTemplate( createBrowserWindowTemplate(view, "window url") ); window.setMenu(menu); browserWindows.set(title, window); window.on("close", () => { view.webContents?.destroy(); browserWindows.delete(title); window = null; }); } ); //* Handle executable from main and renderer ipcMain.on(kFile, async (program) => { const data = { event: "open", ...program }; stdinWrite(data); }); ipcMain.handle(kFile, async (_event, program) => { const data = { event: "open", ...program }; stdinWrite(data); }); //* Handle close ipcMain.handle(kCloseFile, async (_event, program) => { const { file, windowProcess, fallback_path: fallbackFile } = program; try { await execPromise( `taskkill /f /IM ${windowProcess || path.basename(file)} ${ fallbackFile ? ` /IM ${path.basename(fallbackFile)}` : "" }` ); // eslint-disable-next-line no-empty } catch (e) {} }); //* Handle self toolwindow const toolwindowHandler = ({ route, posX, width, height }) => { let { width: screenW, height: screenH } = screen.getPrimaryDisplay().size; const dpi = screen.getPrimaryDisplay().scaleFactor || 1; screenW = Math.round(screenW * dpi); screenH = Math.round(screenH * dpi); const [w, h] = [ Math.round(width ?? screenW > 1025 ? screenW / dpi / 2.5 : 500), Math.round(height ?? screenH > 769 ? screenH / dpi / 3 : 250), ]; let x = 0; let inactivityInterval; switch (posX) { case "left": x = 0; break; case "right": x = screenW - w; break; case "center": x = screenW / 2 - w / 2; break; default: { if (typeof posX === "number") { posX *= dpi; x = posX + (w * dpi) / 2 > screenW ? screenW - w * dpi : posX - (w * dpi) / 2; break; } x = screenW - w * dpi; break; } } const { y: taskbarY, height: taskbarH } = mainWindow.getBounds(); const targetY = Math.round( taskbarY >= h ? taskbarY - h : taskbarY + taskbarH ); x = Math.round(x / dpi); const slideOut = (_win) => { slidingToolWindows.add(route); let opacity = 0.0; let y = Math.round(screenH); _win.setOpacity(opacity); _win.show(); const interval = setInterval(() => { opacity += 0.02; y -= 25; if (y <= targetY) { _win?.setBounds({ x, y: targetY, width: w, height: h }); _win?.setOpacity(1); clearInterval(interval); slidingToolWindows.delete(route); _win.focus(); } else { _win?.setBounds({ x, y, width: w, height: h }); _win?.setOpacity(opacity); } }, 10); }; const slideIn = (_win) => { let opacity = 1; let y = targetY; const { x: winX } = _win.getBounds(); slidingToolWindows.add(route); const interval = setInterval(() => { opacity -= 0.02; y += 25; if (y >= screenH) { _win?.setBounds({ x: winX, y: screenH, width: w, height: h }); _win?.setOpacity(1); clearInterval(interval); slidingToolWindows.delete(route); _win.hide(); } else { _win?.setBounds({ x: winX, y, width: w, height: h }); _win?.setOpacity(opacity); } }, 10); }; const toggleWin = (_win) => { if (_win.isVisible()) { slideIn(_win); return false; } slideOut(_win); return true; }; if (browserWindows.has(route)) { if (!slidingToolWindows.has(route)) { const _win = browserWindows.get(route); // prevent error if win was killed try { _win.focus(); toggleWin(_win); return true; } catch (e) { slidingToolWindows.delete(route); browserWindows.delete(route); } } return false; } // Init window only once const win = new BrowserWindow({ x, y: screenH, width: w, height: h, show: false, frame: false, transparent: false, thickFrame: false, alwaysOnTop: true, type: "toolbar", backgroundColor: "#2e2c29", titleBarStyle: "hidden", opacity: 0, movable: isDev, resizable: isDev, webPreferences: { preload: path.join(app.getAppPath(), "./dist/preload.js"), nodeIntegration: true, contextIsolation: false, backgroundThrottling: false, devTools: isDev, additionalArguments: [route], }, }); win.hide(); browserWindows.set(route, win); if (isDev) { win.loadURL(`http://localhost:4000#/${route}`); } else { win.loadURL( `file://${app.getAppPath()}/dist/renderer/index.html#/${route}` ); } const toolwindowCallback = (_e, a) => { if (a.event === "openDevTools") { if (a.window === route) openDevToolsWindow({ window: win, title: `${route} toolwindow` }); } }; ipcMain.on("toolwindow", toolwindowCallback); win.on("closed", () => { // ipcMain.removeListener("toolwindow", toolwindowCallback) ipcMain.removeAllListeners(["toolwindow"]); slidingToolWindows.delete(route); browserWindows.delete(route); }); win.on("show", () => { win.webContents.send("window", "show"); inactivityInterval = setInterval(() => { if (powerMonitor.getSystemIdleTime() >= 60 && win.isVisible()) { clearInterval(inactivityInterval); win.blur(); } }, 10 * 1000); }); win.on("close", (e) => { e.preventDefault(); clearInterval(inactivityInterval); win.blur(); }); win.on("blur", (e) => { if (!isDev) { e.preventDefault(); if (!slidingToolWindows.has(route)) { toggleWin(win); } } }); return true; }; ipcMain.handle(kToolWindow, async (_event, args) => toolwindowHandler(args)); //* Handle Focus window ipcMain.handle(kFocus, async (_event, hwnd) => WinActivate(hwnd)); //* Init toolwindows setTimeout(() => toolwindowHandler({ route: "utilities" }), 10000); setTimeout(() => toolwindowHandler({ route: "links" }), 15000); }