diff --git a/.prettierignore b/.prettierignore index 75752bc..15e3fb5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,5 @@ coverage *.tsbuildinfo bun.lock apps/tauri/src-tauri +apps/electron/release licenses diff --git a/apps/electron/package.json b/apps/electron/package.json index accc2d8..aa4d5a0 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -60,7 +60,11 @@ } ], "linux": { - "target": ["AppImage", "deb", "rpm"], + "target": [ + "AppImage", + "deb", + "rpm" + ], "icon": "build/icons", "executableName": "tensamin", "category": "Network", @@ -73,11 +77,16 @@ } }, "win": { - "target": ["nsis", "portable"], + "target": [ + "nsis", + "portable" + ], "icon": "build/icons/icon.ico" }, "mac": { - "target": ["dmg"], + "target": [ + "dmg" + ], "icon": "build/icons/icon.icns" }, "publish": null diff --git a/apps/electron/scripts/generate-release-metadata.ts b/apps/electron/scripts/generate-release-metadata.ts index 36a6281..7e36096 100644 --- a/apps/electron/scripts/generate-release-metadata.ts +++ b/apps/electron/scripts/generate-release-metadata.ts @@ -1,5 +1,11 @@ import { createHash } from "node:crypto"; -import { createReadStream, existsSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import { + createReadStream, + existsSync, + readdirSync, + statSync, + writeFileSync, +} from "node:fs"; import { basename, join } from "node:path"; import rootPackage from "../../../package.json" with { type: "json" }; @@ -55,7 +61,10 @@ const metadata = { artifacts, }; -writeFileSync(join(outDir, "electron-release-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`); +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`, diff --git a/apps/electron/src/main/main.ts b/apps/electron/src/main/main.ts index c4ee840..05f88fc 100644 --- a/apps/electron/src/main/main.ts +++ b/apps/electron/src/main/main.ts @@ -1,9 +1,19 @@ 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 { + app, + BrowserWindow, + desktopCapturer, + ipcMain, + session, + shell, +} from "electron"; import { checkForUpdates } from "./updates.js"; -import { ipcChannels, type DesktopScreenShareCapabilities } from "../shared/ipc.js"; +import { + ipcChannels, + type DesktopScreenShareCapabilities, +} from "../shared/ipc.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const verbose = process.argv.includes("--verbose"); @@ -16,7 +26,11 @@ if (verbose) { app.commandLine.appendSwitch("log-level", "0"); } -if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland" && !process.env.TENSAMIN_ENABLE_VULKAN) { +if ( + process.platform === "linux" && + process.env.XDG_SESSION_TYPE === "wayland" && + !process.env.TENSAMIN_ENABLE_VULKAN +) { app.commandLine.appendSwitch("disable-features", "Vulkan"); } @@ -87,7 +101,8 @@ async function listAudioOutputs() { 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; + const name = + typeof record.description === "string" ? record.description : id; if (!id || !name) return null; return { id, name, isDefault: false }; }) @@ -113,30 +128,34 @@ async function listScreenShareSources() { } function registerDisplayMediaHandler() { - session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => { - verboseLog("display media request", { selectedScreenShareSourceId }); + 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 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]; + const selected = sources.find( + (source) => source.id === selectedScreenShareSourceId, + ); + selectedScreenShareSourceId = null; + const video = selected ?? sources[0]; - if (!video) { - callback({}); - return; - } + if (!video) { + callback({}); + return; + } - if (process.platform === "win32") { - callback({ video, audio: "loopback" }); - return; - } + if (process.platform === "win32") { + callback({ video, audio: "loopback" }); + return; + } - callback({ video }); - }); + callback({ video }); + }, + ); } function registerIpc() { @@ -144,16 +163,26 @@ function registerIpc() { 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."); - } + 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; - }); + 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, () => { @@ -217,18 +246,24 @@ async function createWindow() { }); 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( + "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-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()); diff --git a/apps/electron/src/main/updates.ts b/apps/electron/src/main/updates.ts index 7386118..b5f7376 100644 --- a/apps/electron/src/main/updates.ts +++ b/apps/electron/src/main/updates.ts @@ -3,13 +3,21 @@ 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"; +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 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) { @@ -42,7 +50,9 @@ function requestText(url: string): Promise { 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}.`)); + reject( + new Error(`Update request failed with HTTP ${response.statusCode}.`), + ); return; } @@ -86,7 +96,9 @@ async function sha256File(filePath: string) { return hash.digest("hex"); } -function selectArtifact(metadata: ReleaseMetadata): ReleaseArtifact | undefined { +function selectArtifact( + metadata: ReleaseMetadata, +): ReleaseArtifact | undefined { const platform = platformName(); const arch = archName(); @@ -102,16 +114,26 @@ export async function checkForUpdates(): Promise { return { available: false, currentVersion, latestVersion: currentVersion }; } - const metadata = JSON.parse(await requestText(metadataUrl)) as ReleaseMetadata; + const metadata = JSON.parse( + await requestText(metadataUrl), + ) as ReleaseMetadata; const artifact = selectArtifact(metadata); if (isDevVersion(metadata.version) !== isDevVersion(currentVersion)) { - return { available: false, currentVersion, latestVersion: metadata.version }; + return { + available: false, + currentVersion, + latestVersion: metadata.version, + }; } if (isDevVersion(currentVersion)) { if (!artifact || metadata.version === currentVersion) { - return { available: false, currentVersion, latestVersion: metadata.version }; + return { + available: false, + currentVersion, + latestVersion: metadata.version, + }; } return { @@ -123,7 +145,11 @@ export async function checkForUpdates(): Promise { } if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) { - return { available: false, currentVersion, latestVersion: metadata.version }; + return { + available: false, + currentVersion, + latestVersion: metadata.version, + }; } return { @@ -140,7 +166,9 @@ export async function downloadVerifiedArtifact(artifact: ReleaseArtifact) { await mkdir(updatesDir, { recursive: true }); const destination = join(updatesDir, basename(artifact.name)); - await writeFile(destination, await requestBuffer(artifact.url), { mode: 0o600 }); + await writeFile(destination, await requestBuffer(artifact.url), { + mode: 0o600, + }); const actualHash = await sha256File(destination); if (actualHash !== artifact.sha256) { diff --git a/apps/tauri/logo.json b/apps/tauri/logo.json index 9cd2fcb..4c85e3b 100644 --- a/apps/tauri/logo.json +++ b/apps/tauri/logo.json @@ -5,4 +5,4 @@ "android_bg": "./background.png", "android_fg_scale": 100, "android_monochrome": "./monochrome.png" -} \ No newline at end of file +} diff --git a/apps/tauri/scripts/sync-electron-icons.ts b/apps/tauri/scripts/sync-electron-icons.ts index db5cc76..62f232b 100644 --- a/apps/tauri/scripts/sync-electron-icons.ts +++ b/apps/tauri/scripts/sync-electron-icons.ts @@ -8,12 +8,24 @@ const clientDir = resolve(tauriDir, "../.."); const tauriIconsDir = join(tauriDir, "src-tauri", "icons"); const electronIconsDir = join(clientDir, "apps", "electron", "build", "icons"); -const desktopIcons = ["32x32.png", "64x64.png", "128x128.png", "128x128@2x.png", "icon.png", "icon.ico", "icon.icns"]; +const desktopIcons = [ + "32x32.png", + "64x64.png", + "128x128.png", + "128x128@2x.png", + "icon.png", + "icon.ico", + "icon.icns", +]; await mkdir(electronIconsDir, { recursive: true }); await Promise.all( - desktopIcons.map((icon) => copyFile(join(tauriIconsDir, icon), join(electronIconsDir, icon))), + desktopIcons.map((icon) => + copyFile(join(tauriIconsDir, icon), join(electronIconsDir, icon)), + ), ); -console.log(`Synced ${desktopIcons.length} desktop icons to ${electronIconsDir}`); +console.log( + `Synced ${desktopIcons.length} desktop icons to ${electronIconsDir}`, +); diff --git a/eslint.config.ts b/eslint.config.ts index 2209bf9..19d24ae 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -10,7 +10,7 @@ const tsconfigRootDir = dirname(fileURLToPath(import.meta.url)); export default [ { - ignores: ["**/dist/**", "**/node_modules/**", "**/.tmp/**"], + ignores: ["**/dist/**", "**/node_modules/**"], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/packages/shared/src/desktopMedia.tsx b/packages/shared/src/desktopMedia.tsx index d117716..4da0119 100644 --- a/packages/shared/src/desktopMedia.tsx +++ b/packages/shared/src/desktopMedia.tsx @@ -26,7 +26,9 @@ type ElectronDesktopApi = { media?: { getScreenShareCapabilities?: () => Promise; listScreenShareSources?: () => Promise; - listScreenShareAudioOutputs?: () => Promise; + listScreenShareAudioOutputs?: () => Promise< + DesktopScreenShareAudioOutput[] + >; selectScreenShareSource?: (sourceId: string) => Promise; }; }; diff --git a/scripts/copy-releases.ts b/scripts/copy-releases.ts index 3b0eeec..194228a 100644 --- a/scripts/copy-releases.ts +++ b/scripts/copy-releases.ts @@ -60,7 +60,9 @@ const electronReleaseDir = "apps/electron/release"; const copyElectronArtifacts = () => { if (!existsSync(electronReleaseDir)) { - console.warn(`Skipping Electron artifacts: ${electronReleaseDir} does not exist.`); + console.warn( + `Skipping Electron artifacts: ${electronReleaseDir} does not exist.`, + ); return; } @@ -81,7 +83,11 @@ const artifacts = await Promise.all( readdirSync(releasesDir) .map((file) => join(releasesDir, file)) .filter((file) => statSync(file).isFile()) - .filter((file) => !file.endsWith("electron-release-metadata.json") && !file.endsWith("SHA256SUMS")) + .filter( + (file) => + !file.endsWith("electron-release-metadata.json") && + !file.endsWith("SHA256SUMS"), + ) .map(async (filePath) => { const name = filePath.split(/[\\/]/).at(-1)!; diff --git a/tsconfig.json b/tsconfig.json index 0096dc3..5fdf2a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2020", "module": "nodenext", "lib": ["ES2020"], - "rootDir": "./scripts", + "rootDir": ".", "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -12,6 +12,6 @@ "moduleResolution": "nodenext", "types": ["node"] }, - "include": ["scripts"], + "include": ["scripts", "eslint.config.ts"], "exclude": ["node_modules", "dist"] }