Compare commits

..
24 changed files with 68 additions and 303 deletions

View file

@ -1,56 +0,0 @@
on:
workflow_dispatch:
schedule:
- cron: "30 3 * * *"
jobs:
cleanup-workflow-runs:
runs-on: docker
steps:
- name: Install Packages
run: apt-get update && apt-get install -y curl jq
- name: Delete workflow runs older than 14 days
env:
TOKEN: ${{ forgejo.token }}
API: ${{ forgejo.api_url }}
REPO: ${{ forgejo.repository }}
CURRENT_RUN_ID: ${{ forgejo.run_id }}
run: |
set -eu
CUTOFF="$(date -u -d "14 days ago" +"%Y-%m-%dT%H:%M:%SZ")"
PAGE=1
DELETE_IDS=delete-workflow-runs.txt
: > "$DELETE_IDS"
while :; do
curl -fsS \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/actions/runs?page=$PAGE&limit=50" \
-o runs.json
COUNT="$(jq '.workflow_runs | length' runs.json)"
test "$COUNT" -gt 0 || break
jq -r \
--arg cutoff "$CUTOFF" \
--arg current "$CURRENT_RUN_ID" \
'.workflow_runs[]
| select((.id | tostring) != $current)
| select(["success", "failure", "cancelled", "skipped", "succeeded", "failed"] | index(.status))
| select(.created < $cutoff)
| .id' runs.json \
>> "$DELETE_IDS"
PAGE="$((PAGE + 1))"
done
while IFS= read -r run_id; do
test -n "$run_id" || continue
echo "Deleting workflow run $run_id"
curl -fsS -X DELETE \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/actions/runs/$run_id"
done < "$DELETE_IDS"

View file

@ -232,46 +232,6 @@ jobs:
-F "attachment=@$file" -F "attachment=@$file"
done done
- name: Delete dev releases for prod version
env:
TOKEN: ${{ forgejo.token }}
API: ${{ forgejo.api_url }}
REPO: ${{ forgejo.repository }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -eu
PAGE=1
PREFIX="$TAG-dev-"
DELETE_RELEASES=delete-dev-releases.tsv
: > "$DELETE_RELEASES"
while :; do
curl -fsS \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases?page=$PAGE&limit=50&pre-release=true" \
-o releases.json
COUNT="$(jq 'length' releases.json)"
test "$COUNT" -gt 0 || break
jq -r \
--arg prefix "$PREFIX" \
'.[] | select(.prerelease == true) | select(.tag_name | startswith($prefix)) | [.id, .tag_name] | @tsv' releases.json \
>> "$DELETE_RELEASES"
PAGE="$((PAGE + 1))"
done
while IFS="$(printf '\t')" read -r release_id release_tag; do
test -n "$release_id" || continue
echo "Deleting dev release $release_tag"
curl -fsS -X DELETE \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$release_id"
done < "$DELETE_RELEASES"
- name: Update root flake release hash - name: Update root flake release hash
env: env:
TAG: ${{ steps.version.outputs.tag }} TAG: ${{ steps.version.outputs.tag }}

View file

@ -5,5 +5,4 @@ coverage
*.tsbuildinfo *.tsbuildinfo
bun.lock bun.lock
apps/tauri/src-tauri apps/tauri/src-tauri
apps/electron/release
licenses licenses

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -41,7 +41,6 @@
"productName": "Tensamin", "productName": "Tensamin",
"executableName": "tensamin", "executableName": "tensamin",
"artifactName": "Tensamin-${version}-${os}-${arch}.${ext}", "artifactName": "Tensamin-${version}-${os}-${arch}.${ext}",
"icon": "build/icons/icon.png",
"directories": { "directories": {
"output": "release" "output": "release"
}, },
@ -53,19 +52,10 @@
{ {
"from": "../web/dist", "from": "../web/dist",
"to": "web" "to": "web"
},
{
"from": "build/icons/icon.png",
"to": "icons/icon.png"
} }
], ],
"linux": { "linux": {
"target": [ "target": ["AppImage", "deb", "rpm"],
"AppImage",
"deb",
"rpm"
],
"icon": "build/icons",
"executableName": "tensamin", "executableName": "tensamin",
"category": "Network", "category": "Network",
"maintainer": "Methanium", "maintainer": "Methanium",
@ -77,17 +67,10 @@
} }
}, },
"win": { "win": {
"target": [ "target": ["nsis", "portable"]
"nsis",
"portable"
],
"icon": "build/icons/icon.ico"
}, },
"mac": { "mac": {
"target": [ "target": ["dmg"]
"dmg"
],
"icon": "build/icons/icon.icns"
}, },
"publish": null "publish": null
} }

View file

@ -1,11 +1,5 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { import { createReadStream, existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
createReadStream,
existsSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import rootPackage from "../../../package.json" with { type: "json" }; import rootPackage from "../../../package.json" with { type: "json" };
@ -61,10 +55,7 @@ const metadata = {
artifacts, artifacts,
}; };
writeFileSync( writeFileSync(join(outDir, "electron-release-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
join(outDir, "electron-release-metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
);
writeFileSync( writeFileSync(
join(outDir, "SHA256SUMS"), join(outDir, "SHA256SUMS"),
`${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`, `${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`,

View file

@ -1,19 +1,9 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { import { app, BrowserWindow, desktopCapturer, ipcMain, session, shell } from "electron";
app,
BrowserWindow,
desktopCapturer,
ipcMain,
session,
shell,
} from "electron";
import { checkForUpdates } from "./updates.js"; import { checkForUpdates } from "./updates.js";
import { import { ipcChannels, type DesktopScreenShareCapabilities } from "../shared/ipc.js";
ipcChannels,
type DesktopScreenShareCapabilities,
} from "../shared/ipc.js";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const verbose = process.argv.includes("--verbose"); const verbose = process.argv.includes("--verbose");
@ -26,11 +16,7 @@ if (verbose) {
app.commandLine.appendSwitch("log-level", "0"); app.commandLine.appendSwitch("log-level", "0");
} }
if ( if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland" && !process.env.TENSAMIN_ENABLE_VULKAN) {
process.platform === "linux" &&
process.env.XDG_SESSION_TYPE === "wayland" &&
!process.env.TENSAMIN_ENABLE_VULKAN
) {
app.commandLine.appendSwitch("disable-features", "Vulkan"); app.commandLine.appendSwitch("disable-features", "Vulkan");
} }
@ -48,12 +34,6 @@ function getRendererIndex() {
return join(process.resourcesPath, "web", "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"] { function getPlatform(): DesktopScreenShareCapabilities["platform"] {
if (process.platform === "linux") return "linux"; if (process.platform === "linux") return "linux";
if (process.platform === "darwin") return "macos"; if (process.platform === "darwin") return "macos";
@ -101,8 +81,7 @@ async function listAudioOutputs() {
if (!sink || typeof sink !== "object") return null; if (!sink || typeof sink !== "object") return null;
const record = sink as Record<string, unknown>; const record = sink as Record<string, unknown>;
const id = record.index == null ? undefined : String(record.index); const id = record.index == null ? undefined : String(record.index);
const name = const name = typeof record.description === "string" ? record.description : id;
typeof record.description === "string" ? record.description : id;
if (!id || !name) return null; if (!id || !name) return null;
return { id, name, isDefault: false }; return { id, name, isDefault: false };
}) })
@ -128,8 +107,7 @@ async function listScreenShareSources() {
} }
function registerDisplayMediaHandler() { function registerDisplayMediaHandler() {
session.defaultSession.setDisplayMediaRequestHandler( session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
async (_request, callback) => {
verboseLog("display media request", { selectedScreenShareSourceId }); verboseLog("display media request", { selectedScreenShareSourceId });
const sources = await desktopCapturer.getSources({ const sources = await desktopCapturer.getSources({
@ -137,9 +115,7 @@ function registerDisplayMediaHandler() {
thumbnailSize: { width: 0, height: 0 }, thumbnailSize: { width: 0, height: 0 },
}); });
const selected = sources.find( const selected = sources.find((source) => source.id === selectedScreenShareSourceId);
(source) => source.id === selectedScreenShareSourceId,
);
selectedScreenShareSourceId = null; selectedScreenShareSourceId = null;
const video = selected ?? sources[0]; const video = selected ?? sources[0];
@ -154,8 +130,7 @@ function registerDisplayMediaHandler() {
} }
callback({ video }); callback({ video });
}, });
);
} }
function registerIpc() { function registerIpc() {
@ -163,26 +138,16 @@ function registerIpc() {
ipcMain.handle(ipcChannels.listScreenShareSources, listScreenShareSources); ipcMain.handle(ipcChannels.listScreenShareSources, listScreenShareSources);
ipcMain.handle(ipcChannels.listScreenShareAudioOutputs, listAudioOutputs); ipcMain.handle(ipcChannels.listScreenShareAudioOutputs, listAudioOutputs);
ipcMain.handle( ipcMain.handle(ipcChannels.getScreenShareCapabilities, getScreenShareCapabilities);
ipcChannels.getScreenShareCapabilities, ipcMain.handle(ipcChannels.selectScreenShareSource, (_event, sourceId: unknown) => {
getScreenShareCapabilities, if (typeof sourceId !== "string" || sourceId.length === 0 || sourceId.length > 256) {
);
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."); throw new Error("Invalid screen share source id.");
} }
selectedScreenShareSourceId = sourceId; selectedScreenShareSourceId = sourceId;
verboseLog("selected screen share source", sourceId); verboseLog("selected screen share source", sourceId);
return true; return true;
}, });
);
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion()); ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates); ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
ipcMain.handle(ipcChannels.minimizeWindow, () => { ipcMain.handle(ipcChannels.minimizeWindow, () => {
@ -226,7 +191,6 @@ async function createWindow() {
minWidth: 900, minWidth: 900,
minHeight: 600, minHeight: 600,
title: "Tensamin", title: "Tensamin",
icon: getWindowIcon(),
frame: false, frame: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
@ -246,24 +210,18 @@ async function createWindow() {
}); });
if (verbose) { if (verbose) {
mainWindow.webContents.on( mainWindow.webContents.on("console-message", (_event, level, message, line, sourceId) => {
"console-message",
(_event, level, message, line, sourceId) => {
const target = level >= 2 ? console.error : console.log; const target = level >= 2 ? console.error : console.log;
target("[tensamin:renderer]", message, { level, line, sourceId }); target("[tensamin:renderer]", message, { level, line, sourceId });
}, });
);
mainWindow.webContents.on( mainWindow.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => {
"did-fail-load",
(_event, errorCode, errorDescription, validatedURL) => {
console.error("[tensamin:electron] renderer failed to load", { console.error("[tensamin:electron] renderer failed to load", {
errorCode, errorCode,
errorDescription, errorDescription,
validatedURL, validatedURL,
}); });
}, });
);
mainWindow.webContents.on("did-finish-load", () => { mainWindow.webContents.on("did-finish-load", () => {
verboseLog("renderer finished loading", mainWindow?.webContents.getURL()); verboseLog("renderer finished loading", mainWindow?.webContents.getURL());

View file

@ -3,21 +3,13 @@ import { createReadStream } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { app, net } from "electron"; import { app, net } from "electron";
import type { import type { ReleaseArtifact, ReleaseMetadata, UpdateCheckResult } from "../shared/ipc.js";
ReleaseArtifact,
ReleaseMetadata,
UpdateCheckResult,
} from "../shared/ipc.js";
const metadataUrl = process.env.TENSAMIN_UPDATE_METADATA_URL; const metadataUrl = process.env.TENSAMIN_UPDATE_METADATA_URL;
function compareSemver(left: string, right: string) { function compareSemver(left: string, right: string) {
const leftParts = left const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
.split(/[.-]/) const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
.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); const length = Math.max(leftParts.length, rightParts.length);
for (let index = 0; index < length; index += 1) { for (let index = 0; index < length; index += 1) {
@ -50,9 +42,7 @@ function requestText(url: string): Promise<string> {
const request = net.request(url); const request = net.request(url);
request.on("response", (response) => { request.on("response", (response) => {
if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode < 200 || response.statusCode >= 300) {
reject( reject(new Error(`Update request failed with HTTP ${response.statusCode}.`));
new Error(`Update request failed with HTTP ${response.statusCode}.`),
);
return; return;
} }
@ -96,9 +86,7 @@ async function sha256File(filePath: string) {
return hash.digest("hex"); return hash.digest("hex");
} }
function selectArtifact( function selectArtifact(metadata: ReleaseMetadata): ReleaseArtifact | undefined {
metadata: ReleaseMetadata,
): ReleaseArtifact | undefined {
const platform = platformName(); const platform = platformName();
const arch = archName(); const arch = archName();
@ -114,26 +102,16 @@ export async function checkForUpdates(): Promise<UpdateCheckResult> {
return { available: false, currentVersion, latestVersion: currentVersion }; return { available: false, currentVersion, latestVersion: currentVersion };
} }
const metadata = JSON.parse( const metadata = JSON.parse(await requestText(metadataUrl)) as ReleaseMetadata;
await requestText(metadataUrl),
) as ReleaseMetadata;
const artifact = selectArtifact(metadata); const artifact = selectArtifact(metadata);
if (isDevVersion(metadata.version) !== isDevVersion(currentVersion)) { if (isDevVersion(metadata.version) !== isDevVersion(currentVersion)) {
return { return { available: false, currentVersion, latestVersion: metadata.version };
available: false,
currentVersion,
latestVersion: metadata.version,
};
} }
if (isDevVersion(currentVersion)) { if (isDevVersion(currentVersion)) {
if (!artifact || metadata.version === currentVersion) { if (!artifact || metadata.version === currentVersion) {
return { return { available: false, currentVersion, latestVersion: metadata.version };
available: false,
currentVersion,
latestVersion: metadata.version,
};
} }
return { return {
@ -145,11 +123,7 @@ export async function checkForUpdates(): Promise<UpdateCheckResult> {
} }
if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) { if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) {
return { return { available: false, currentVersion, latestVersion: metadata.version };
available: false,
currentVersion,
latestVersion: metadata.version,
};
} }
return { return {
@ -166,9 +140,7 @@ export async function downloadVerifiedArtifact(artifact: ReleaseArtifact) {
await mkdir(updatesDir, { recursive: true }); await mkdir(updatesDir, { recursive: true });
const destination = join(updatesDir, basename(artifact.name)); const destination = join(updatesDir, basename(artifact.name));
await writeFile(destination, await requestBuffer(artifact.url), { await writeFile(destination, await requestBuffer(artifact.url), { mode: 0o600 });
mode: 0o600,
});
const actualHash = await sha256File(destination); const actualHash = await sha256File(destination);
if (actualHash !== artifact.sha256) { if (actualHash !== artifact.sha256) {

View file

@ -22,7 +22,7 @@
"build:mobile:raw": "tauri android build", "build:mobile:raw": "tauri android build",
"dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi", "dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi",
"build:mobile": "bun run render-version.ts && if command -v nix >/dev/null 2>&1; then nix develop --command bun build:mobile:raw; else bun build:mobile:raw; fi && bun run render-version.ts --unrender", "build:mobile": "bun run render-version.ts && if command -v nix >/dev/null 2>&1; then nix develop --command bun build:mobile:raw; else bun build:mobile:raw; fi && bun run render-version.ts --unrender",
"gen-icons": "tauri icon ./logo.json && bun scripts/sync-electron-icons.ts", "gen-icons": "tauri icon ./logo.json",
"format": "bunx prettier --write .", "format": "bunx prettier --write .",
"lint": "eslint src" "lint": "eslint src"
}, },

View file

@ -1,31 +0,0 @@
import { copyFile, mkdir } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const tauriDir = resolve(scriptDir, "..");
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",
];
await mkdir(electronIconsDir, { recursive: true });
await Promise.all(
desktopIcons.map((icon) =>
copyFile(join(tauriIconsDir, icon), join(electronIconsDir, icon)),
),
);
console.log(
`Synced ${desktopIcons.length} desktop icons to ${electronIconsDir}`,
);

Binary file not shown.

View file

@ -10,7 +10,7 @@ const tsconfigRootDir = dirname(fileURLToPath(import.meta.url));
export default [ export default [
{ {
ignores: ["**/dist/**", "**/node_modules/**"], ignores: ["**/dist/**", "**/node_modules/**", "**/.tmp/**"],
}, },
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,

View file

@ -1,6 +1,6 @@
{ {
"name": "tensamin", "name": "tensamin",
"version": "0.0.7", "version": "0.0.6",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"packages/*", "packages/*",

View file

@ -26,9 +26,7 @@ type ElectronDesktopApi = {
media?: { media?: {
getScreenShareCapabilities?: () => Promise<DesktopScreenShareCapabilities>; getScreenShareCapabilities?: () => Promise<DesktopScreenShareCapabilities>;
listScreenShareSources?: () => Promise<DesktopScreenShareSource[]>; listScreenShareSources?: () => Promise<DesktopScreenShareSource[]>;
listScreenShareAudioOutputs?: () => Promise< listScreenShareAudioOutputs?: () => Promise<DesktopScreenShareAudioOutput[]>;
DesktopScreenShareAudioOutput[]
>;
selectScreenShareSource?: (sourceId: string) => Promise<boolean>; selectScreenShareSource?: (sourceId: string) => Promise<boolean>;
}; };
}; };

View file

@ -1,3 +0,0 @@
# Information
All dev releases between two prod version get deleted upon creation of the latest prod release.

View file

@ -60,9 +60,7 @@ const electronReleaseDir = "apps/electron/release";
const copyElectronArtifacts = () => { const copyElectronArtifacts = () => {
if (!existsSync(electronReleaseDir)) { if (!existsSync(electronReleaseDir)) {
console.warn( console.warn(`Skipping Electron artifacts: ${electronReleaseDir} does not exist.`);
`Skipping Electron artifacts: ${electronReleaseDir} does not exist.`,
);
return; return;
} }
@ -83,11 +81,7 @@ const artifacts = await Promise.all(
readdirSync(releasesDir) readdirSync(releasesDir)
.map((file) => join(releasesDir, file)) .map((file) => join(releasesDir, file))
.filter((file) => statSync(file).isFile()) .filter((file) => statSync(file).isFile())
.filter( .filter((file) => !file.endsWith("electron-release-metadata.json") && !file.endsWith("SHA256SUMS"))
(file) =>
!file.endsWith("electron-release-metadata.json") &&
!file.endsWith("SHA256SUMS"),
)
.map(async (filePath) => { .map(async (filePath) => {
const name = filePath.split(/[\\/]/).at(-1)!; const name = filePath.split(/[\\/]/).at(-1)!;

View file

@ -3,7 +3,7 @@
"target": "ES2020", "target": "ES2020",
"module": "nodenext", "module": "nodenext",
"lib": ["ES2020"], "lib": ["ES2020"],
"rootDir": ".", "rootDir": "./scripts",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -12,6 +12,6 @@
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"types": ["node"] "types": ["node"]
}, },
"include": ["scripts", "eslint.config.ts"], "include": ["scripts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }