production-taskbar-client / src / main / ipc / windowsChannel.js
windowsChannel.js
Raw
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);
}