Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 93 additions & 7 deletions src/common/flags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { powerMonitor } 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";

interface Preset {
Expand All @@ -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"],
Expand Down Expand Up @@ -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

Expand All @@ -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();
}
6 changes: 6 additions & 0 deletions src/discord/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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";
Expand Down Expand Up @@ -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);
});
Expand Down
2 changes: 2 additions & 0 deletions src/discord/preload/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { AppliedFlagsOutput } from "../../main.js";
import type { venmicListObject } from "../venmic.js";
let windowCallback: (arg0: object) => void;

Expand Down Expand Up @@ -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),
Expand Down
21 changes: 9 additions & 12 deletions src/discord/preload/patches.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
1 change: 1 addition & 0 deletions src/discord/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export function createWindow() {
browserWindowOptions.titleBarStyle = "hidden";
browserWindowOptions.titleBarOverlay = false;
}
break;
case "native":
browserWindowOptions.frame = true;
break;
Expand Down
99 changes: 84 additions & 15 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string, string | boolean>;
enableFeatures: string[];
disableFeatures: string[];
enableBlinkFeatures: string[];
disableBlinkFeatures: string[];
}

const tracker = {
switches: new Map<string, string | boolean>(),
enableFeatures: new Set<string>(),
disableFeatures: new Set<string>(),
enableBlinkFeatures: new Set<string>(),
disableBlinkFeatures: new Set<string>(),
};

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";
Expand Down Expand Up @@ -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") {
Expand All @@ -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));
}
Expand Down Expand Up @@ -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)
Expand Down
Loading