From 5908f3905cc30027c0b041d02cab0fdfc0e1783f Mon Sep 17 00:00:00 2001 From: AnuragRai Date: Sat, 14 Feb 2026 16:40:21 +0530 Subject: [PATCH 1/3] feat: add custom Chromium flags support with runtime flag dump (#1016) This adds a user-facing way to extend Chromium startup flags through a local flags.json file in the app data directory. The selected legcord preset and custom flags are merged at startup. The final applied switches and features can then be inspected through dumpFlags for easier troubleshooting. This also fixes additionalArguments parsing so values passed as --key=value are handled correctly. --- src/common/flags.ts | 100 +++++++++++++++++++++++++++++++--- src/discord/ipc.ts | 6 ++ src/discord/preload/bridge.ts | 2 + src/main.ts | 99 ++++++++++++++++++++++++++++----- 4 files changed, 185 insertions(+), 22 deletions(-) diff --git a/src/common/flags.ts b/src/common/flags.ts index d5337bb5..bd72d7eb 100644 --- a/src/common/flags.ts +++ b/src/common/flags.ts @@ -1,4 +1,7 @@ -import { powerMonitor } from "electron"; +import { powerMonitor, app } from "electron"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import isDev from "electron-is-dev"; import { getConfig } from "./config.js"; interface Preset { @@ -7,6 +10,9 @@ interface Preset { disableFeatures: string[]; } +// Cache for custom flags to avoid repeated file reads +let customFlagsCache: Preset | null = null; + const performance: Preset = { switches: [ ["enable-gpu-rasterization"], @@ -81,6 +87,79 @@ const vaapi: Preset = { disableFeatures: ["UseChromeOSDirectVideoDecoder"], }; +/** + * Load custom flags from JSON file in user data directory (cached after first load) + * Path: + * - Windows: %APPDATA%\legcord\flags.json (typically C:\Users\{username}\AppData\Roaming\legcord\flags.json) + * - macOS: ~/Library/Application Support/legcord/flags.json + * - Linux: ~/.config/legcord/flags.json + * Returns an empty preset if file doesn't exist or is invalid + */ +function loadCustomFlags(): Preset { + // Return cached result to avoid repeated disk reads + if (customFlagsCache !== null) { + return customFlagsCache; + } + + const customPreset: Preset = { + switches: [], + enableFeatures: [], + disableFeatures: [], + }; + + try { + const userDataPath = app.getPath("userData"); + const customFlagsPath = join(userDataPath, "flags.json"); + + try { + const fileContent = readFileSync(customFlagsPath, "utf-8"); + const customFlags = JSON.parse(fileContent); + + // Merge switches + if (Array.isArray(customFlags.switches)) { + customPreset.switches = customFlags.switches; + } + + // Merge enableFeatures + if (Array.isArray(customFlags.enableFeatures)) { + customPreset.enableFeatures = customFlags.enableFeatures; + } + + // Merge disableFeatures + if (Array.isArray(customFlags.disableFeatures)) { + customPreset.disableFeatures = customFlags.disableFeatures; + } + + if (isDev) console.log(`Custom flags loaded from ${customFlagsPath}`); + } catch (fileError) { + if ((fileError as NodeJS.ErrnoException).code === "ENOENT") { + if (isDev) console.log(`Custom flags file not found at ${customFlagsPath}`); + } else if (isDev) { + console.error(`Error reading custom flags file: ${fileError}`); + } + } + } catch (error) { + if (isDev) console.error(`Error loading custom flags: ${error}`); + } + + customFlagsCache = customPreset; + return customPreset; +} + +/** + * Merge a preset with custom flags + * Custom flags will be appended to the preset's arrays + */ +function mergeWithCustomFlags(preset: Preset): Preset { + const customFlags = loadCustomFlags(); + + return { + switches: [...preset.switches, ...customFlags.switches], + enableFeatures: [...preset.enableFeatures, ...customFlags.enableFeatures], + disableFeatures: [...preset.disableFeatures, ...customFlags.disableFeatures], + }; +} + export function getPreset(): Preset | undefined { // MIT License @@ -107,24 +186,31 @@ export function getPreset(): Preset | undefined { case "dynamic": if (powerMonitor.isOnBatteryPower()) { console.log("Battery mode enabled"); - return battery; + return mergeWithCustomFlags(battery); } else { console.log("Performance mode enabled"); - return performance; + return mergeWithCustomFlags(performance); } case "performance": console.log("Performance mode enabled"); - return performance; + return mergeWithCustomFlags(performance); case "battery": console.log("Battery mode enabled"); - return battery; + return mergeWithCustomFlags(battery); case "vaapi": console.log("VAAPI mode enabled"); - return vaapi; + return mergeWithCustomFlags(vaapi); case "smoothScreenshare": console.log("Smooth screenshare mode enabled"); - return smoothExperiment; + return mergeWithCustomFlags(smoothExperiment); default: console.log("No performance modes set"); } } + +/** + * Get the currently applied preset for debugging purposes + */ +export function getCurrentPreset(): Preset | undefined { + return getPreset(); +} diff --git a/src/discord/ipc.ts b/src/discord/ipc.ts index 6da8a7d6..cc9dd7d6 100644 --- a/src/discord/ipc.ts +++ b/src/discord/ipc.ts @@ -8,6 +8,7 @@ import type { Keybind } from "../@types/keybind.js"; import type { Settings } from "../@types/settings.js"; import type { ThemeManifest } from "../@types/themeManifest.js"; import { getConfig, getConfigLocation, setConfig, setConfigBulk } from "../common/config.js"; +import { getAppliedFlags } from "../main.js"; import { addDetectable, getDetectables, removeDetectable } from "../common/detectables.js"; import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js"; import { installTheme, setThemeEnabled, uninstallTheme } from "../common/themes.js"; @@ -227,6 +228,11 @@ export function registerIpc(passedWindow: BrowserWindow): void { ipcMain.on("isDev", (event) => { event.returnValue = isDev; }); + ipcMain.on("dumpFlags", (event) => { + const flags = getAppliedFlags(); + console.log(`=== Chrome Flags === ${JSON.stringify(flags)}`); + event.returnValue = flags; + }); ipcMain.on("setConfig", (_event, key: keyof Settings, value: string) => { setConfig(key, value); }); diff --git a/src/discord/preload/bridge.ts b/src/discord/preload/bridge.ts index f7a0d92d..715704b9 100644 --- a/src/discord/preload/bridge.ts +++ b/src/discord/preload/bridge.ts @@ -5,6 +5,7 @@ import type { LegcordWindow } from "../../@types/legcordWindow.d.ts"; import type { Settings } from "../../@types/settings.js"; import type { ThemeManifest } from "../../@types/themeManifest.js"; import type { venmicListObject } from "../venmic.js"; +import type { AppliedFlagsOutput } from "../../main.js"; let windowCallback: (arg0: object) => void; interface IPCSources { @@ -36,6 +37,7 @@ contextBridge.exposeInMainWorld("legcord", { openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"), copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"), copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"), + dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput, }, touchbar: { setVoiceTouchbar: (state: boolean) => ipcRenderer.send("setVoiceTouchbar", state), diff --git a/src/main.ts b/src/main.ts index 5c8c80a4..d8639d00 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ // Modules to control application life and create native browser window import { BrowserWindow, app, crashReporter, session, systemPreferences } from "electron"; +import isDev from "electron-is-dev"; import "./discord/extensions/csp.js"; import "./protocol.js"; import { readFileSync } from "node:fs"; @@ -18,6 +19,40 @@ import { import "./updater.js"; import { getPreset } from "./common/flags.js"; import { setLang } from "./common/lang.js"; + +// Chrome flags tracking +export interface AppliedFlagsOutput { + switches: Record; + enableFeatures: string[]; + disableFeatures: string[]; + enableBlinkFeatures: string[]; + disableBlinkFeatures: string[]; +} + +const tracker = { + switches: new Map(), + enableFeatures: new Set(), + disableFeatures: new Set(), + enableBlinkFeatures: new Set(), + disableBlinkFeatures: new Set(), +}; + +const trackSwitch = (key: string, value?: string) => tracker.switches.set(key, value ?? true); +const trackEnableFeatures = (features: string[]) => features.forEach(f => tracker.enableFeatures.add(f)); +const trackDisableFeatures = (features: string[]) => features.forEach(f => tracker.disableFeatures.add(f)); +const trackEnableBlinkFeatures = (features: string[]) => features.forEach(f => tracker.enableBlinkFeatures.add(f)); +const trackDisableBlinkFeatures = (features: string[]) => features.forEach(f => tracker.disableBlinkFeatures.add(f)); + +export function getAppliedFlags(): AppliedFlagsOutput { + return { + switches: Object.fromEntries(tracker.switches), + enableFeatures: Array.from(tracker.enableFeatures), + disableFeatures: Array.from(tracker.disableFeatures), + enableBlinkFeatures: Array.from(tracker.enableBlinkFeatures), + disableBlinkFeatures: Array.from(tracker.disableBlinkFeatures), + }; +} + import { fetchMods } from "./discord/extensions/modloader.js"; import { createWindow } from "./discord/window.js"; import { createSetupWindow } from "./setup/main.js"; @@ -98,9 +133,11 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { // enable pulseaudio audio sharing on linux if (process.platform === "linux") { app.commandLine.appendSwitch("gtk-version", "3"); + trackSwitch("gtk-version", "3"); enableFeatures.add("PulseaudioLoopbackForScreenShare"); disableFeatures.add("WebRtcAllowInputVolumeAdjustment"); app.commandLine.appendSwitch("enable-speech-dispatcher"); + trackSwitch("enable-speech-dispatcher"); } // enable webrtc capturer for wayland if (process.platform === "linux" && process.env.XDG_SESSION_TYPE?.toLowerCase() === "wayland") { @@ -114,12 +151,17 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { } // work around chrome 66 disabling autoplay by default app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); + trackSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("enable-transparent-visuals"); + trackSwitch("enable-transparent-visuals"); checkIfConfigIsBroken(); const preset = getPreset(); if (preset) { - preset.switches.forEach(([key, val]) => app.commandLine.appendSwitch(key, val)); + preset.switches.forEach(([key, val]) => { + app.commandLine.appendSwitch(key, val); + trackSwitch(key, val); + }); preset.enableFeatures.forEach((val) => enableFeatures.add(val)); preset.disableFeatures.forEach((val) => disableFeatures.add(val)); } @@ -162,42 +204,69 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) { if (getConfig("additionalArguments") !== undefined) { for (const arg of getConfig("additionalArguments").split(" ")) { if (arg.startsWith("--")) { - const [key, val] = arg.substring(2).split("=", 1) as [string, string?]; + const [key, ...rest] = arg.substring(2).split("="); + const val = rest.length > 0 ? rest.join("=") : undefined; if (val === undefined) { app.commandLine.appendSwitch(key); + trackSwitch(key); } else { if (key === "enable-features") { - val.split(",").forEach((flag) => enableFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => enableFeatures.add(flag)); } else if (key === "disable-features") { - val.split(",").forEach((flag) => disableFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => disableFeatures.add(flag)); } else if (key === "enable-blink-features") { - val.split(",").forEach((flag) => enableBlinkFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => enableBlinkFeatures.add(flag)); } else if (key === "disable-blink-features") { - val.split(",").forEach((flag) => disableBlinkFeatures.add(flag)); + const flags = val.split(","); + flags.forEach((flag) => disableBlinkFeatures.add(flag)); } else { app.commandLine.appendSwitch(key, val); + trackSwitch(key, val); } } } } } - if (getConfig("smoothScroll") === false) app.commandLine.appendSwitch("disable-smooth-scrolling"); + if (getConfig("smoothScroll") === false) { + app.commandLine.appendSwitch("disable-smooth-scrolling"); + trackSwitch("disable-smooth-scrolling"); + } if (getConfig("autoScroll")) enableBlinkFeatures.add("MiddleClickAutoscroll"); - if (getConfig("disableHttpCache")) app.commandLine.appendSwitch("disable-http-cache"); + if (getConfig("disableHttpCache")) { + app.commandLine.appendSwitch("disable-http-cache"); + trackSwitch("disable-http-cache"); + } enableFeatures.delete(""); disableFeatures.delete(""); enableBlinkFeatures.delete(""); disableBlinkFeatures.delete(""); - if (enableFeatures.size > 0) app.commandLine.appendSwitch("enable-features", Array.from(enableFeatures).join(",")); - if (disableFeatures.size > 0) - app.commandLine.appendSwitch("disable-features", Array.from(disableFeatures).join(",")); - if (enableBlinkFeatures.size > 0) - app.commandLine.appendSwitch("enable-blink-features", Array.from(enableBlinkFeatures).join(",")); - if (disableBlinkFeatures.size > 0) - app.commandLine.appendSwitch("disable-blink-features", Array.from(disableBlinkFeatures).join(",")); + if (enableFeatures.size > 0) { + const featuresStr = Array.from(enableFeatures).join(","); + app.commandLine.appendSwitch("enable-features", featuresStr); + trackEnableFeatures(Array.from(enableFeatures)); + } + if (disableFeatures.size > 0) { + const featuresStr = Array.from(disableFeatures).join(","); + app.commandLine.appendSwitch("disable-features", featuresStr); + trackDisableFeatures(Array.from(disableFeatures)); + } + if (enableBlinkFeatures.size > 0) { + const featuresStr = Array.from(enableBlinkFeatures).join(","); + app.commandLine.appendSwitch("enable-blink-features", featuresStr); + trackEnableBlinkFeatures(Array.from(enableBlinkFeatures)); + } + if (disableBlinkFeatures.size > 0) { + const featuresStr = Array.from(disableBlinkFeatures).join(","); + app.commandLine.appendSwitch("disable-blink-features", featuresStr); + trackDisableBlinkFeatures(Array.from(disableBlinkFeatures)); + } void app.whenReady().then(async () => { + if (isDev) console.log(JSON.stringify(getAppliedFlags())); process.on("SIGINT", () => app.quit()); process.on("SIGTERM", () => app.quit()); // Patch for linux bug to ensure things are loaded before window creation (fixes transparency on some linux systems) From 32b51342ad97ec0462f955492d6c2dada7912f7b Mon Sep 17 00:00:00 2001 From: MahmodZE <97690050+MahmodZE@users.noreply.github.com> Date: Sat, 14 Feb 2026 03:11:44 -0800 Subject: [PATCH 2/3] fix: settings menu version patch (#1014) * fix legcord version patching * micro optimization --- src/discord/preload/patches.mts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/discord/preload/patches.mts b/src/discord/preload/patches.mts index 449fe6dc..305f1933 100644 --- a/src/discord/preload/patches.mts +++ b/src/discord/preload/patches.mts @@ -63,21 +63,18 @@ async function load() { }); injectJS("legcord://assets/js/patchVencordQuickCSS.js"); // Settings info version injection - setInterval(() => { - const host = document.querySelector('[class*="sidebar"] [class*="info"]'); - if (!host || host.querySelector("#ac-ver")) { - return; - } + const observer = new MutationObserver(() => { + if (document.body.querySelector("#ac-ver")) return; - const discordVersionInfoPattern = /(stable|ptb|canary) \d+|Electron|Chromium/i; - if (!discordVersionInfoPattern.test(host.textContent || "")) { - return; - } + const info = document.body.querySelector('[class*="sidebar"] [class*="compactInfo"]'); + const host = info?.parentElement; + if (!host || !/(stable|ptb|canary) \d+|Electron|Chromium/i.test(host.textContent)) return; - const el = host.firstElementChild!.cloneNode() as HTMLSpanElement; + const el = host.querySelector("span")!.cloneNode() as HTMLSpanElement; el.id = "ac-ver"; el.textContent = `Legcord Version: ${version}`; - host.append(el); - }, 1000); + info.after(el); + }); + observer.observe(document.body, { childList: true, subtree: true }); } load(); From a91cc6da5cdc16a06988b9de3417fa7d7acbab8b Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:14:43 +0100 Subject: [PATCH 3/3] chore: linting --- src/common/flags.ts | 2 +- src/discord/ipc.ts | 2 +- src/discord/preload/bridge.ts | 2 +- src/discord/window.ts | 1 + src/main.ts | 8 ++++---- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/common/flags.ts b/src/common/flags.ts index bd72d7eb..4de320e8 100644 --- a/src/common/flags.ts +++ b/src/common/flags.ts @@ -1,6 +1,6 @@ -import { powerMonitor, app } from "electron"; import { readFileSync } from "node:fs"; import { join } from "node:path"; +import { app, powerMonitor } from "electron"; import isDev from "electron-is-dev"; import { getConfig } from "./config.js"; diff --git a/src/discord/ipc.ts b/src/discord/ipc.ts index cc9dd7d6..2fd38fce 100644 --- a/src/discord/ipc.ts +++ b/src/discord/ipc.ts @@ -8,12 +8,12 @@ import type { Keybind } from "../@types/keybind.js"; import type { Settings } from "../@types/settings.js"; import type { ThemeManifest } from "../@types/themeManifest.js"; import { getConfig, getConfigLocation, setConfig, setConfigBulk } from "../common/config.js"; -import { getAppliedFlags } from "../main.js"; import { addDetectable, getDetectables, removeDetectable } from "../common/detectables.js"; import { getLang, getLangName, getRawLang, setLang } from "../common/lang.js"; import { installTheme, setThemeEnabled, uninstallTheme } from "../common/themes.js"; import { getDisplayVersion, getVersion } from "../common/version.js"; import { openCssEditor } from "../cssEditor/main.js"; +import { getAppliedFlags } from "../main.js"; import { isPowerSavingEnabled, setPowerSaving } from "../power.js"; import constPaths from "../shared/consts/paths.js"; import { splashWindow } from "../splash/main.js"; diff --git a/src/discord/preload/bridge.ts b/src/discord/preload/bridge.ts index 715704b9..c6ae97e8 100644 --- a/src/discord/preload/bridge.ts +++ b/src/discord/preload/bridge.ts @@ -4,8 +4,8 @@ import type { Keybind } from "../../@types/keybind.js"; import type { LegcordWindow } from "../../@types/legcordWindow.d.ts"; import type { Settings } from "../../@types/settings.js"; import type { ThemeManifest } from "../../@types/themeManifest.js"; -import type { venmicListObject } from "../venmic.js"; import type { AppliedFlagsOutput } from "../../main.js"; +import type { venmicListObject } from "../venmic.js"; let windowCallback: (arg0: object) => void; interface IPCSources { diff --git a/src/discord/window.ts b/src/discord/window.ts index 91863815..ff2713ff 100644 --- a/src/discord/window.ts +++ b/src/discord/window.ts @@ -369,6 +369,7 @@ export function createWindow() { browserWindowOptions.titleBarStyle = "hidden"; browserWindowOptions.titleBarOverlay = false; } + break; case "native": browserWindowOptions.frame = true; break; diff --git a/src/main.ts b/src/main.ts index d8639d00..93769ad6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,10 +38,10 @@ const tracker = { }; const trackSwitch = (key: string, value?: string) => tracker.switches.set(key, value ?? true); -const trackEnableFeatures = (features: string[]) => features.forEach(f => tracker.enableFeatures.add(f)); -const trackDisableFeatures = (features: string[]) => features.forEach(f => tracker.disableFeatures.add(f)); -const trackEnableBlinkFeatures = (features: string[]) => features.forEach(f => tracker.enableBlinkFeatures.add(f)); -const trackDisableBlinkFeatures = (features: string[]) => features.forEach(f => tracker.disableBlinkFeatures.add(f)); +const trackEnableFeatures = (features: string[]) => features.forEach((f) => tracker.enableFeatures.add(f)); +const trackDisableFeatures = (features: string[]) => features.forEach((f) => tracker.disableFeatures.add(f)); +const trackEnableBlinkFeatures = (features: string[]) => features.forEach((f) => tracker.enableBlinkFeatures.add(f)); +const trackDisableBlinkFeatures = (features: string[]) => features.forEach((f) => tracker.disableBlinkFeatures.add(f)); export function getAppliedFlags(): AppliedFlagsOutput { return {