(feat): migrate experimental tauri cef to electron
Some checks failed
/ build-web (push) Successful in 1m19s
/ build-desktop (linux) (push) Failing after 2m33s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled

(feat): add flake to expose tensamin desktop package
(qol): update todo
This commit is contained in:
Alois 2026-05-25 13:16:56 +02:00
commit a209ade10b
32 changed files with 1649 additions and 459 deletions

2
apps/electron/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
dist
release

61
apps/electron/flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1779508470,
"narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "29916453413845e54a65b8a1cf996842300cd299",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

88
apps/electron/flake.nix Normal file
View file

@ -0,0 +1,88 @@
{
description = "Electron Development Environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {nixpkgs, flake-utils, ...}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system; config.allowUnfree = true;};
electronRuntimeLibs = with pkgs; [
alsa-lib
at-spi2-atk
at-spi2-core
atk
cairo
cups
dbus
expat
fontconfig
freetype
gdk-pixbuf
glib
gtk3
libdrm
libgbm
libglvnd
libnotify
libpulseaudio
libuuid
libxkbcommon
mesa
nspr
nss
pango
pipewire
systemd
wayland
# xorg
libX11
libXScrnSaver
libXcomposite
libXcursor
libXdamage
libXext
libXfixes
libXi
libXrandr
libXtst
libxcb
];
in {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodejs_22
corepack_22
bun
electron
pkg-config
python3
gcc
gnumake
git
jq
patchelf
dpkg
rpm
fpm
] ++ electronRuntimeLibs;
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath electronRuntimeLibs}:$LD_LIBRARY_PATH"
export ELECTRON_ENABLE_LOGGING=1
export ELECTRON_OZONE_PLATFORM_HINT="''${ELECTRON_OZONE_PLATFORM_HINT:-auto}"
export NPM_CONFIG_TARGET_ARCH="''${NPM_CONFIG_TARGET_ARCH:-x64}"
export npm_config_build_from_source=true
export USE_SYSTEM_FPM=true
alias electron-install='cd ../.. && bun install'
alias electron-build-web='cd ../.. && bun run build:web'
alias electron-dev='bun run dev'
alias electron-package='bun run package:linux'
alias electron-validate='bun run validate'
'';
};
});
}

View file

@ -0,0 +1,69 @@
{
"name": "@tensamin/electron",
"private": true,
"version": "0.0.3",
"description": "Tensamin desktop client",
"author": "methanium",
"homepage": "https://git.methanium.net/tensamin/client",
"type": "module",
"main": "dist/main/main.js",
"scripts": {
"clean": "rm -rf dist release",
"build:web": "cd ../.. && bun run build:web",
"build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs",
"dev:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron . --verbose",
"dev": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run dev:raw; else bun run dev:raw; fi",
"start:raw": "bun run build && electron .",
"start": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run start:raw; else bun run start:raw; fi",
"package:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --publish never",
"package": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:raw; else bun run package:raw; fi",
"package:linux:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --linux --publish never",
"package:linux": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:linux:raw; else bun run package:linux:raw; fi",
"package:windows:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --win --publish never",
"package:windows": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:windows:raw; else bun run package:windows:raw; fi",
"checksum": "bun scripts/generate-release-metadata.ts",
"generate-signing-key": "bun scripts/generate-signing-key.ts",
"validate:raw": "bun run build && bun run package:linux:raw",
"validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run validate:raw; else bun run validate:raw; fi"
},
"dependencies": {
"electron-updater": "^6.6.2"
},
"devDependencies": {
"@types/node": "^25.9.1",
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"esbuild": "^0.25.11",
"typescript": "~6.0.3"
},
"build": {
"appId": "net.tensamin.client",
"productName": "Tensamin",
"artifactName": "Tensamin-${version}-${os}-${arch}.${ext}",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"package.json"
],
"extraResources": [
{
"from": "../web/dist",
"to": "web"
}
],
"linux": {
"target": ["AppImage", "deb", "rpm"],
"category": "Network",
"maintainer": "methanium"
},
"win": {
"target": ["nsis", "portable"]
},
"mac": {
"target": ["dmg"]
},
"publish": null
}
}

View file

@ -0,0 +1,62 @@
import { createHash } from "node:crypto";
import { createReadStream, existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { basename, join } from "node:path";
import rootPackage from "../../../package.json" with { type: "json" };
const releaseDir = join(import.meta.dir, "..", "release");
const outDir = join(import.meta.dir, "..", "..", "..", "releases");
function sha256(filePath: string) {
const hash = createHash("sha256");
const stream = createReadStream(filePath);
return new Promise<string>((resolve, reject) => {
stream.on("data", (chunk) => hash.update(chunk));
stream.on("error", reject);
stream.on("end", () => resolve(hash.digest("hex")));
});
}
function platformFor(file: string) {
if (/win|nsis|portable|\.exe$/i.test(file)) return "windows";
if (/mac|darwin|\.dmg$/i.test(file)) return "macos";
return "linux";
}
function archFor(file: string) {
if (/arm64|aarch64/i.test(file)) return "arm64";
return "x64";
}
if (!existsSync(releaseDir)) {
throw new Error(`Missing Electron release directory: ${releaseDir}`);
}
const files = readdirSync(releaseDir)
.filter((file) => !file.endsWith(".blockmap") && !file.endsWith(".yml"))
.map((file) => join(releaseDir, file))
.filter((file) => statSync(file).isFile());
const artifacts = await Promise.all(
files.map(async (filePath) => ({
name: basename(filePath),
platform: platformFor(filePath),
arch: archFor(filePath),
url: `__FORGEJO_RELEASE_ASSET_URL__/${encodeURIComponent(basename(filePath))}`,
sha256: await sha256(filePath),
size: statSync(filePath).size,
})),
);
const metadata = {
version: rootPackage.version,
tag: rootPackage.version,
publishedAt: new Date().toISOString(),
artifacts,
};
writeFileSync(join(outDir, "electron-release-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
writeFileSync(
join(outDir, "SHA256SUMS"),
`${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`,
);

View file

@ -0,0 +1,11 @@
import { generateKeyPairSync } from "node:crypto";
const { privateKey, publicKey } = generateKeyPairSync("ed25519", {
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
});
console.log("TENSAMIN_UPDATE_PRIVATE_KEY_PEM=");
console.log(privateKey.trim());
console.log("\nTENSAMIN_UPDATE_PUBLIC_KEY_PEM=");
console.log(publicKey.trim());

View file

@ -0,0 +1,274 @@
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 DesktopScreenShareCapabilities } from "../shared/ipc.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const verbose = process.argv.includes("--verbose");
let mainWindow: BrowserWindow | null = null;
let selectedScreenShareSourceId: string | null = null;
if (verbose) {
app.commandLine.appendSwitch("enable-logging", "stderr");
app.commandLine.appendSwitch("v", "1");
app.commandLine.appendSwitch("log-level", "0");
}
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 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<unknown>((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() {
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<string, unknown>;
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(Boolean);
}
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 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.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",
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();
await createWindow();
}
void start().catch((error) => {
console.error("[tensamin:electron] failed to start", error);
app.exit(1);
});

View file

@ -0,0 +1,131 @@
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { app, net } from "electron";
import type { ReleaseArtifact, ReleaseMetadata, UpdateCheckResult } from "../shared/ipc.js";
const metadataUrl = process.env.TENSAMIN_UPDATE_METADATA_URL;
function compareSemver(left: string, right: string) {
const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
const length = Math.max(leftParts.length, rightParts.length);
for (let index = 0; index < length; index += 1) {
const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
if (diff !== 0) return diff;
}
return 0;
}
function platformName() {
if (process.platform === "win32") return "windows";
if (process.platform === "darwin") return "macos";
if (process.platform === "linux") return "linux";
return process.platform;
}
function archName() {
if (process.arch === "x64") return "x64";
if (process.arch === "arm64") return "arm64";
return process.arch;
}
function requestText(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const request = net.request(url);
request.on("response", (response) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
reject(new Error(`Update request failed with HTTP ${response.statusCode}.`));
return;
}
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
request.on("error", reject);
request.end();
});
}
function requestBuffer(url: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
const request = net.request(url);
request.on("response", (response) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
reject(new Error(`Download failed with HTTP ${response.statusCode}.`));
return;
}
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => resolve(Buffer.concat(chunks)));
});
request.on("error", reject);
request.end();
});
}
async function sha256File(filePath: string) {
const hash = createHash("sha256");
await new Promise<void>((resolve, reject) => {
const stream = createReadStream(filePath);
stream.on("data", (chunk) => hash.update(chunk));
stream.on("error", reject);
stream.on("end", resolve);
});
return hash.digest("hex");
}
function selectArtifact(metadata: ReleaseMetadata): ReleaseArtifact | undefined {
const platform = platformName();
const arch = archName();
return metadata.artifacts.find(
(artifact) => artifact.platform === platform && artifact.arch === arch,
);
}
export async function checkForUpdates(): Promise<UpdateCheckResult> {
const currentVersion = app.getVersion();
if (!metadataUrl) {
return { available: false, currentVersion, latestVersion: currentVersion };
}
const metadata = JSON.parse(await requestText(metadataUrl)) as ReleaseMetadata;
const artifact = selectArtifact(metadata);
if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) {
return { available: false, currentVersion, latestVersion: metadata.version };
}
return {
available: true,
currentVersion,
latestVersion: metadata.version,
artifact,
};
}
export async function downloadVerifiedArtifact(artifact: ReleaseArtifact) {
const updatesDir = join(app.getPath("userData"), "updates");
await rm(updatesDir, { recursive: true, force: true });
await mkdir(updatesDir, { recursive: true });
const destination = join(updatesDir, basename(artifact.name));
await writeFile(destination, await requestBuffer(artifact.url), { mode: 0o600 });
const actualHash = await sha256File(destination);
if (actualHash !== artifact.sha256) {
await rm(destination, { force: true });
throw new Error("Downloaded update failed checksum verification.");
}
return destination;
}

View file

@ -0,0 +1,43 @@
import { contextBridge, ipcRenderer } from "electron";
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js";
function windowAction(channel: string) {
return () => ipcRenderer.invoke(channel);
}
const desktopApi = {
media: {
listScreenShareSources: () =>
ipcRenderer.invoke(ipcChannels.listScreenShareSources),
listScreenShareAudioOutputs: () =>
ipcRenderer.invoke(ipcChannels.listScreenShareAudioOutputs),
getScreenShareCapabilities: () =>
ipcRenderer.invoke(ipcChannels.getScreenShareCapabilities),
selectScreenShareSource: (sourceId: DesktopScreenShareSource["id"]) => {
if (typeof sourceId !== "string" || sourceId.length === 0) {
return Promise.reject(new Error("Invalid screen share source id."));
}
return ipcRenderer.invoke(ipcChannels.selectScreenShareSource, sourceId);
},
},
app: {
getVersion: () => ipcRenderer.invoke(ipcChannels.getVersion),
},
updates: {
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
},
window: {
minimize: () => windowAction(ipcChannels.minimizeWindow),
maximize: () => windowAction(ipcChannels.maximizeWindow),
close: () => windowAction(ipcChannels.closeWindow),
},
};
contextBridge.exposeInMainWorld("tensaminDesktop", desktopApi);
contextBridge.exposeInMainWorld(
"tensaminShowWindowControls",
process.env.TENSAMIN_HIDE_CONTROLS == null,
);
export type TensaminDesktopApi = typeof desktopApi;

View file

@ -0,0 +1,58 @@
export type DesktopScreenShareSource = {
id: string;
kind: "screen" | "window";
name: string;
subtitle?: string | null;
thumbnail?: string | null;
};
export type DesktopScreenShareAudioOutput = {
id: string;
name: string;
isDefault: boolean;
};
export type DesktopScreenShareCapabilities = {
runtime: "electron";
platform: "linux" | "macos" | "windows" | "other";
showAudioOutputSelector: boolean;
showAudioSwitch: boolean;
hasReliableSystemAudio: boolean;
};
export type ReleaseArtifact = {
name: string;
platform: string;
arch: string;
url: string;
sha256: string;
size: number;
};
export type ReleaseMetadata = {
version: string;
tag: string;
publishedAt: string;
artifacts: ReleaseArtifact[];
};
export type UpdateCheckResult =
| { available: false; currentVersion: string; latestVersion: string }
| {
available: true;
currentVersion: string;
latestVersion: string;
artifact: ReleaseArtifact;
};
export const ipcChannels = {
listScreenShareSources: "desktopMedia:listScreenShareSources",
listScreenShareAudioOutputs: "desktopMedia:listScreenShareAudioOutputs",
getScreenShareCapabilities: "desktopMedia:getScreenShareCapabilities",
selectScreenShareSource: "desktopMedia:selectScreenShareSource",
minimizeWindow: "window:minimize",
maximizeWindow: "window:maximize",
closeWindow: "window:close",
getVersion: "app:getVersion",
checkForUpdates: "updates:checkForUpdates",
} as const;

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"types": ["node", "electron"]
},
"include": ["src/**/*.ts"]
}