feat: Add game activity status (Experimental)
All checks were successful
Queue Release Build / prepare (push) Successful in 23s
Deploy Web Apps / deploy (push) Successful in 5m54s
Queue Release Build / build-windows (push) Successful in 16m19s
Queue Release Build / build-linux (push) Successful in 30m13s
Queue Release Build / finalize (push) Successful in 47s

This commit is contained in:
2026-04-27 05:46:33 +02:00
parent 3858beb28e
commit 53389ed3ad
50 changed files with 2007 additions and 104 deletions

View File

@@ -16,6 +16,7 @@ Electron main-process package for MetoYou / Toju. This directory owns desktop bo
| --- | --- |
| `main.ts` | Electron app bootstrap and process entry point |
| `preload.ts` | Typed renderer-facing preload bridge |
| `process-list.ts` | Linux/Windows process-name scan used by now-playing game detection |
| `app/` | App lifecycle and startup composition |
| `ipc/` | Renderer-invoked IPC handlers |
| `cqrs/` | Local database command/query handlers and mappings |
@@ -28,4 +29,4 @@ Electron main-process package for MetoYou / Toju. This directory owns desktop bo
- When adding a renderer-facing capability, update the Electron implementation, `preload.ts`, and the renderer bridge in `toju-app/` together.
- Treat `dist/electron/` and `dist-electron/` as generated output.
- See [AGENTS.md](AGENTS.md) for package-level editing rules.
- See [AGENTS.md](AGENTS.md) for package-level editing rules.

View File

@@ -55,6 +55,7 @@ import {
importUserData,
openCurrentDataFolder
} from '../data-management';
import { listRunningProcessNames } from '../process-list';
const DEFAULT_MIME_TYPE = 'application/octet-stream';
const FILE_CLIPBOARD_FORMATS = [
@@ -320,6 +321,8 @@ export function setupSystemHandlers(): void {
}
});
ipcMain.handle('get-running-process-names', async () => await listRunningProcessNames());
ipcMain.handle('prepare-linux-screen-share-audio-routing', async () => {
return await prepareLinuxScreenShareAudioRouting();
});

View File

@@ -167,6 +167,7 @@ export interface ElectronAPI {
openExternal: (url: string) => Promise<boolean>;
getSources: () => Promise<{ id: string; name: string; thumbnail: string }[]>;
getRunningProcessNames: () => Promise<string[]>;
prepareLinuxScreenShareAudioRouting: () => Promise<LinuxScreenShareAudioRoutingInfo>;
activateLinuxScreenShareAudioRouting: () => Promise<LinuxScreenShareAudioRoutingInfo>;
deactivateLinuxScreenShareAudioRouting: () => Promise<boolean>;
@@ -252,6 +253,7 @@ const electronAPI: ElectronAPI = {
openExternal: (url) => ipcRenderer.invoke('open-external', url),
getSources: () => ipcRenderer.invoke('get-sources'),
getRunningProcessNames: () => ipcRenderer.invoke('get-running-process-names'),
prepareLinuxScreenShareAudioRouting: () => ipcRenderer.invoke('prepare-linux-screen-share-audio-routing'),
activateLinuxScreenShareAudioRouting: () => ipcRenderer.invoke('activate-linux-screen-share-audio-routing'),
deactivateLinuxScreenShareAudioRouting: () => ipcRenderer.invoke('deactivate-linux-screen-share-audio-routing'),

85
electron/process-list.ts Normal file
View File

@@ -0,0 +1,85 @@
import { execFile } from 'child_process';
import * as path from 'path';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
const MAX_PROCESS_NAMES = 512;
export async function listRunningProcessNames(): Promise<string[]> {
if (process.platform === 'win32') {
return normalizeProcessNames(await listWindowsProcessNames());
}
if (process.platform === 'linux') {
return normalizeProcessNames(await listLinuxProcessNames());
}
return [];
}
async function listLinuxProcessNames(): Promise<string[]> {
const { stdout } = await execFileAsync('ps', ['-eo', 'comm='], {
maxBuffer: 1024 * 1024,
timeout: 5_000
});
return stdout.split('\n');
}
async function listWindowsProcessNames(): Promise<string[]> {
const { stdout } = await execFileAsync('tasklist', [
'/FO',
'CSV',
'/NH'
], {
maxBuffer: 1024 * 1024,
timeout: 5_000,
windowsHide: true
});
return stdout
.split(/\r?\n/)
.map((line) => parseCsvFirstColumn(line));
}
function parseCsvFirstColumn(line: string): string {
const trimmed = line.trim();
if (!trimmed) {
return '';
}
if (!trimmed.startsWith('"')) {
return trimmed.split(',')[0] ?? '';
}
const endQuoteIndex = trimmed.indexOf('"', 1);
return endQuoteIndex > 1 ? trimmed.slice(1, endQuoteIndex) : '';
}
function normalizeProcessNames(names: string[]): string[] {
const normalized = new Set<string>();
for (const rawName of names) {
const name = normalizeProcessName(rawName);
if (name) {
normalized.add(name);
}
}
return Array.from(normalized)
.sort()
.slice(0, MAX_PROCESS_NAMES);
}
function normalizeProcessName(rawName: string): string {
const baseName = path.basename(rawName.trim()).trim();
if (!baseName || baseName.length > 96) {
return '';
}
return baseName;
}