Equicord/src/equicordplugins/commandPalette/registry.tsx
Ethan Davies e00c4d84cd
feat(CommandPalette): Guild & DM Nav (#539)
* feat(CommandPalette): add guilds navigation options

* feat(CommandPalette): add DM friends options

* chore(CommandPalette): remove unused css file used in testing

* Remove console log from command handler

Removed console log for transitioning to direct messages.

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
2025-12-07 15:34:47 -05:00

2777 lines
91 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { isPluginEnabled } from "@api/PluginManager";
import { SettingsStore } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { openPluginModal } from "@components/settings/tabs";
import { toggleEnabled } from "@equicordplugins/equicordHelper/utils";
import type { Plugin } from "@utils/types";
import { changes, checkForUpdates } from "@utils/updater";
import { Guild } from "@vencord/discord-types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ChannelActionCreators, ChannelRouter, ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, MediaEngineStore, NavigationRouter, openUserSettingsPanel, React, ReadStateUtils, RelationshipStore, SelectedChannelStore, SelectedGuildStore, StreamerModeStore, Toasts, useEffect, UserStore, VoiceActions } from "@webpack/common";
import type { FC, ReactElement, ReactNode } from "react";
import { Settings } from "Vencord";
type CommandHandler = () => void | Promise<void>;
type ToastKind = (typeof Toasts.Type)[keyof typeof Toasts.Type];
export interface CommandCategory {
id: string;
label: string;
description?: string;
parentId?: string | null;
}
export interface CommandEntry {
id: string;
label: string;
description?: string;
keywords?: string[];
tags?: string[];
categoryId?: string;
shortcut?: string | null;
handler: CommandHandler;
danger?: boolean;
hiddenInSearch?: boolean;
searchGroup?: string;
}
export interface CommandTagMeta {
id: string;
label: string;
count: number;
}
interface ClosedDmSnapshot {
channelId?: string | null;
recipientId?: string | null;
isGroupDm?: boolean;
}
interface ChatBarCommandState {
commandId: string;
label: string;
getters: Set<() => HTMLElement | null>;
}
export const chatBarCommandStates = new Map<string, ChatBarCommandState>();
export const chatBarCommandStatesById = new Map<string, ChatBarCommandState>();
const CHATBAR_DATASET_KEY = "vcCommandPaletteId";
const CHATBAR_DATA_ATTRIBUTE = "data-vc-command-palette-id";
const categories = new Map<string, CommandCategory>();
const registry = new Map<string, CommandEntry>();
let registryVersion = 0;
let cachedSortedCommands: CommandEntry[] | null = null;
const categoryCommandCache = new Map<string, CommandEntry[]>();
const treeCommandCache = new Map<string, CommandEntry[]>();
const searchTextCache = new Map<string, string>();
const registryListeners = new Set<(version: number) => void>();
const RECENT_COMMAND_LIMIT = 10;
const recentCommandIds: string[] = [];
const RECENT_COMMAND_SKIP_IDS = new Set<string>([
"command-palette-rerun-last",
"command-palette-toggle-pin-last",
"command-palette-show-recent",
"command-palette-open-settings"
]);
const StatusSetting = getUserSettingLazy<string>("status", "status");
const COMMAND_PALETTE_PLUGIN_NAME = "CommandPalette";
const CUSTOM_COMMANDS_KEY = "CommandPaletteCustomCommands";
const CUSTOM_COMMANDS_CATEGORY_ID = "custom-commands";
const SESSION_TOOLS_CATEGORY_ID = "session-tools";
const CONTEXT_PROVIDER_ID = "context-current";
const CUSTOM_PROVIDER_ID = "custom-commands";
const TOOLBOX_ACTIONS_CATEGORY_ID = "plugins-actions";
const TOOLBOX_ACTIONS_PROVIDER_ID = "plugin-toolbox-actions";
const CHATBAR_ACTIONS_CATEGORY_ID = "chatbar-actions";
const GUILD_CATEGORY_ID = "guilds-actions";
const FRIENDS_CATEGORY_ID = "friends-actions";
const commandTagIds = new Map<string, string[]>();
const tagMetadata = new Map<string, { label: string; count: number; }>();
const TAG_CORE = "Core";
const TAG_NAVIGATION = "Navigation";
const TAG_UTILITY = "Utility";
const TAG_DEVELOPER = "Developer";
const TAG_CUSTOMIZATION = "Customization";
const TAG_PLUGINS = "Plugins";
const TAG_SESSION = "Session";
const TAG_CONTEXT = "Context";
const TAG_CUSTOM = "Custom";
const TAG_GUILDS = "Guilds";
const TAG_FRIENDS = "Friends";
export function normalizeTag(tag: string): string {
return tag.trim().toLowerCase();
}
export function wrapChatBarChildren(children: ReactNode): ReactNode {
if (!Array.isArray(children) || children.length === 0) return children;
let hasChanges = false;
const wrappedChildren = children.map((child, index) => {
if (!React.isValidElement(child)) return child;
const childElement = child as ReactElement;
if (childElement.type === ChatBarCommandBridge) return child;
const existingKey = typeof childElement.key === "string" && childElement.key.length > 0
? childElement.key
: undefined;
const buttonKey = existingKey ?? `chatbar-${index}`;
hasChanges = true;
const bridgeProps: ChatBarCommandBridgeElementProps = {
element: childElement,
buttonKey,
key: existingKey ?? buttonKey
};
return (
<ChatBarCommandBridge key={existingKey} {...bridgeProps} />
);
});
if (!hasChanges) return children;
return wrappedChildren;
}
interface ChatBarCommandBridgeProps {
element: ReactElement;
buttonKey: string;
}
type ChatBarCommandBridgeElementProps = ChatBarCommandBridgeProps & { key?: string; };
export const ChatBarCommandBridge: FC<ChatBarCommandBridgeProps> = ({ element, buttonKey }) => {
const containerRef = React.useRef<HTMLDivElement | null>(null);
const latestElementRef = React.useRef(element);
latestElementRef.current = element;
useEffect(() => {
let cancelled = false;
let cleanup: (() => void) | null = null;
let timeoutId: number | null = null;
let delay = 16;
const attachToChatBar = () => {
if (cancelled) return;
const container = containerRef.current;
if (!container) {
scheduleRetry();
return;
}
const target = resolveChatBarElementFromContainer(container);
if (!target) {
scheduleRetry();
return;
}
const label = extractChatBarLabel(target, latestElementRef.current, buttonKey);
cleanup = attachChatBarInstance(buttonKey, label, container, target);
delay = 16;
};
const scheduleRetry = () => {
if (cancelled) return;
const currentDelay = delay;
delay = Math.min(delay * 2, 1000);
timeoutId = window.setTimeout(attachToChatBar, currentDelay);
};
attachToChatBar();
return () => {
cancelled = true;
if (timeoutId != null) window.clearTimeout(timeoutId);
cleanup?.();
};
}, [buttonKey]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const state = chatBarCommandStates.get(buttonKey);
if (!state) return;
const target = resolveChatBarElement(state);
if (!target) return;
const label = extractChatBarLabel(target, latestElementRef.current, buttonKey);
ensureChatBarCommandState(buttonKey, label);
}, [element, buttonKey]);
return (
<div ref={containerRef} style={{ display: "contents" }}>
{element}
</div>
);
};
export function resolveChatBarElementFromContainer(container: HTMLElement): HTMLElement | null {
if (container.hasAttribute("aria-label")) return container;
const labelled = container.querySelector<HTMLElement>("[aria-label]");
return labelled ?? null;
}
export function attachChatBarInstance(buttonKey: string, label: string, container: HTMLElement, element: HTMLElement): () => void {
const state = ensureChatBarCommandState(buttonKey, label);
const { commandId } = state;
element.dataset[CHATBAR_DATASET_KEY] = commandId;
element.setAttribute(CHATBAR_DATA_ATTRIBUTE, commandId);
const getter = () => {
if (element.isConnected) return element;
const scoped = container.querySelector<HTMLElement>(`[${CHATBAR_DATA_ATTRIBUTE}="${commandId}"]`);
if (scoped) return scoped;
return document.querySelector<HTMLElement>(`[${CHATBAR_DATA_ATTRIBUTE}="${commandId}"]`);
};
state.getters.add(getter);
return () => {
state.getters.delete(getter);
if (state.getters.size === 0) {
removeCommand(state.commandId);
chatBarCommandStates.delete(buttonKey);
chatBarCommandStatesById.delete(state.commandId);
}
element.removeAttribute(CHATBAR_DATA_ATTRIBUTE);
delete (element.dataset as Record<string, string | undefined>)[CHATBAR_DATASET_KEY];
};
}
export function extractChatBarLabel(element: HTMLElement | null, reactElement: ReactElement | null, fallback: string): string {
const aria = element?.getAttribute("aria-label")?.trim();
if (aria) return aria;
const fromElement = readLabelFromElement(reactElement);
if (fromElement) return fromElement;
return fallback;
}
export function readLabelFromElement(element: ReactElement | null): string | null {
if (!element) return null;
const props = (element.props ?? {}) as Record<string, unknown>;
const tooltip = typeof props.tooltip === "string" ? (props.tooltip as string).trim() : "";
if (tooltip) return tooltip;
const aria = typeof props.ariaLabel === "string" ? (props.ariaLabel as string).trim() : "";
if (aria) return aria;
const children = props.children as ReactNode | undefined;
return readLabelFromChildren(children);
}
export function readLabelFromChildren(children: ReactNode | undefined): string | null {
if (children == null || typeof children === "boolean") return null;
if (typeof children === "string") return children.trim() || null;
if (typeof children === "number") return children.toString();
if (Array.isArray(children)) {
for (const child of children) {
const label = readLabelFromChildren(child);
if (label) return label;
}
return null;
}
if (React.isValidElement(children)) {
return readLabelFromElement(children);
}
return null;
}
export function ensureChatBarCommandState(buttonKey: string, label: string): ChatBarCommandState {
const normalizedLabel = label.trim() || buttonKey;
const existing = chatBarCommandStates.get(buttonKey);
if (existing) {
if (existing.label !== normalizedLabel) {
existing.label = normalizedLabel;
registerCommand(buildChatBarCommand(existing));
}
return existing;
}
const commandId = createUniqueChatBarCommandId(normalizedLabel, buttonKey);
const state: ChatBarCommandState = {
commandId,
label: normalizedLabel,
getters: new Set()
};
chatBarCommandStates.set(buttonKey, state);
chatBarCommandStatesById.set(commandId, state);
registerCommand(buildChatBarCommand(state));
return state;
}
function createUniqueChatBarCommandId(label: string, buttonKey: string): string {
const baseSlug = slugifyActionLabel(label) || slugifyActionLabel(buttonKey) || "chat-button";
let candidate = `chatbar-${baseSlug}`;
let suffix = 2;
while (registry.has(candidate) || chatBarCommandStatesById.has(candidate)) {
candidate = `chatbar-${baseSlug}-${suffix++}`;
}
return candidate;
}
function buildChatBarCommand(state: ChatBarCommandState): CommandEntry {
const keywords = Array.from(new Set([
...state.label.toLowerCase().split(/\s+/).filter(Boolean),
"chat",
"button"
]));
return {
id: state.commandId,
label: state.label,
keywords,
categoryId: CHATBAR_ACTIONS_CATEGORY_ID,
tags: [TAG_PLUGINS, TAG_UTILITY],
handler: () => activateChatBarCommand(state)
} satisfies CommandEntry;
}
export function activateChatBarCommand(state: ChatBarCommandState) {
const element = resolveChatBarElement(state);
if (!element) {
showToast(`Unable to find the "${state.label}" chat button.`, Toasts.Type.FAILURE);
return;
}
try {
element.click();
} catch (error) {
console.error("CommandPalette", "Failed to click chat bar button", state.label, error);
showToast(`Failed to trigger ${state.label}.`, Toasts.Type.FAILURE);
return;
}
showToast(`${state.label} activated.`, Toasts.Type.SUCCESS);
}
export function resolveChatBarElement(state: ChatBarCommandState): HTMLElement | null {
for (const getter of Array.from(state.getters)) {
try {
const candidate = getter();
if (candidate && candidate.isConnected) return candidate;
} catch (error) {
console.error("CommandPalette", "Failed to resolve chat bar button", error);
}
}
return document.querySelector<HTMLElement>(`[${CHATBAR_DATA_ATTRIBUTE}="${state.commandId}"]`);
}
function incrementTagMetadata(tagId: string, label: string) {
if (!tagId) return;
const existing = tagMetadata.get(tagId);
if (existing) {
existing.count += 1;
return;
}
tagMetadata.set(tagId, {
label,
count: 1
});
}
function decrementTagMetadata(tagId: string) {
const existing = tagMetadata.get(tagId);
if (!existing) return;
existing.count -= 1;
if (existing.count <= 0) {
tagMetadata.delete(tagId);
}
}
function extractChannelId(input: unknown): string | null {
if (typeof input === "string") return input;
if (!input || typeof input !== "object") return null;
const candidate = input as Record<string, unknown>;
if (typeof candidate.id === "string") return candidate.id;
if (typeof candidate.channelId === "string") return candidate.channelId;
if (typeof candidate.channel_id === "string") return candidate.channel_id;
if (candidate.channel) return extractChannelId(candidate.channel);
return null;
}
function extractUserId(input: unknown): string | null {
if (!input) return null;
if (typeof input === "string") {
const id = input;
return UserStore?.getUser?.(id) ? id : null;
}
if (Array.isArray(input)) {
for (const value of input) {
const nested = extractUserId(value);
if (nested) return nested;
}
return null;
}
if (typeof input !== "object") return null;
const candidate = input as Record<string, unknown>;
const directFields: Array<keyof typeof candidate> = [
"userId",
"user_id",
"recipientId",
"recipient_id"
];
for (const field of directFields) {
const value = candidate[field];
if (typeof value === "string" && UserStore?.getUser?.(value)) {
return value;
}
}
const recipientArrays: Array<keyof typeof candidate> = ["recipient_ids", "recipients", "rawRecipients"];
for (const key of recipientArrays) {
const value = candidate[key];
if (!Array.isArray(value)) continue;
for (const entry of value) {
const id = extractUserId(entry);
if (id) return id;
}
}
if (candidate.user) {
const nested = extractUserId(candidate.user);
if (nested) return nested;
}
if (candidate.channel) {
const nested = extractUserId(candidate.channel);
if (nested) return nested;
}
return null;
}
function findUserIdInArgs(args: unknown[]): string | null {
for (const arg of args) {
const id = extractUserId(arg);
if (id) return id;
}
return null;
}
function resolveChannelFromPayload(input: unknown): any | null {
if (!input) return null;
if (typeof input === "object") {
const candidate = input as Record<string, unknown>;
if (candidate && typeof candidate.id === "string" && ("type" in candidate || "isDM" in candidate)) {
return candidate;
}
if (candidate.channel) {
const nested = resolveChannelFromPayload(candidate.channel);
if (nested) return nested;
}
if (typeof candidate.channel_id === "string") {
const fallback: Record<string, unknown> = { id: candidate.channel_id };
if (candidate.recipients) fallback.recipients = candidate.recipients;
if (candidate.rawRecipients) fallback.rawRecipients = candidate.rawRecipients;
if (candidate.type) fallback.type = candidate.type;
return fallback;
}
}
const channelId = extractChannelId(input);
if (!channelId) return null;
return ChannelStore.getChannel(channelId) ?? { id: channelId };
}
function deriveDmRecipient(channel: any): string | undefined {
if (!channel) return undefined;
const currentUserId = UserStore?.getCurrentUser?.()?.id;
try {
if (typeof channel.getRecipientId === "function") {
const id = channel.getRecipientId();
if (id && id !== currentUserId) return id;
}
} catch {
/* noop */
}
if (Array.isArray(channel.recipients)) {
for (const value of channel.recipients) {
if (typeof value === "string" && value !== currentUserId) return value;
}
}
if (Array.isArray(channel.rawRecipients)) {
for (const entry of channel.rawRecipients) {
const id = typeof entry === "string"
? entry
: entry && typeof entry === "object" && typeof (entry as { id?: string; }).id === "string"
? (entry as { id: string; }).id
: undefined;
if (id && id !== currentUserId) return id;
}
}
return undefined;
}
function trackClosedChannel(channel: any) {
if (!channel || typeof channel.id !== "string") return;
try {
const isDm = typeof channel.isDM === "function" ? channel.isDM() : channel.type === 1;
const isGroupDm = typeof channel.isGroupDM === "function" ? channel.isGroupDM() : channel.type === 3;
if (!isDm && !isGroupDm) return;
lastClosedDm = {
channelId: channel.id,
recipientId: isDm ? deriveDmRecipient(channel) : undefined,
isGroupDm
};
} catch {
/* noop */
}
}
function trackClosedRecipient(recipientId: string | null | undefined, fallbackChannelId?: string | null) {
if (!recipientId) return;
const currentUserId = UserStore?.getCurrentUser?.()?.id;
if (currentUserId && recipientId === currentUserId) return;
if (lastClosedDm?.isGroupDm) return;
let dmChannelId: string | null = null;
const getDmFromUserId = (ChannelStore as { getDMFromUserId?: (id: string) => string | null; }).getDMFromUserId;
if (typeof getDmFromUserId === "function") {
try {
dmChannelId = getDmFromUserId(recipientId) ?? null;
} catch (error) {
console.error("CommandPalette", "Failed to resolve DM channel", error);
}
}
lastClosedDm = {
channelId: fallbackChannelId ?? dmChannelId ?? null,
recipientId,
isGroupDm: false
};
}
function patchChannelCloseMethods() {
if (channelClosePatched) return;
const actions = ChannelActionCreators as Record<string, unknown> | undefined;
if (!actions) return;
const methodNames = [
"closePrivateChannel",
"closeChannel",
"deletePrivateChannel",
"closeDM"
];
const restorers: Array<() => void> = [];
for (const name of methodNames) {
const original = actions[name];
if (typeof original !== "function") continue;
const patched = function patchedChannelClose(this: unknown, ...args: any[]) {
try {
const channel = resolveChannelFromPayload(args[0]);
trackClosedChannel(channel);
const snapshot = lastClosedDm;
if (!snapshot || !snapshot.recipientId) {
const userId = findUserIdInArgs(args);
if (userId) {
const fallbackChannelId = snapshot?.channelId ?? (typeof channel?.id === "string" ? channel.id : null);
trackClosedRecipient(userId, fallbackChannelId);
}
}
} catch (error) {
console.error("CommandPalette", `Failed to record closed channel via ${name}`, error);
}
return (original as (...inner: any[]) => unknown).apply(this, args);
};
(actions as Record<string, unknown>)[name] = patched;
restorers.push(() => {
(actions as Record<string, unknown>)[name] = original;
});
}
if (!restorers.length) return;
channelClosePatched = true;
sessionCleanupCallbacks.push(() => {
for (const restore of restorers) {
try {
restore();
} catch (error) {
console.error("CommandPalette", "Failed to restore channel close method", error);
}
}
channelClosePatched = false;
});
}
let statusResetTimeout: number | null = null;
let statusResetPrevious: string | null = null;
let sessionHooksInitialized = false;
let lastClosedDm: ClosedDmSnapshot | null = null;
let channelClosePatched = false;
const sessionCleanupCallbacks: Array<() => void> = [];
const runtimeCleanupCallbacks: Array<() => void> = [];
const NotificationSettingsActionCreators = findByPropsLazy("updateGuildNotificationSettings", "updateChannelOverrideSettings");
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
export const DEFAULT_CATEGORY_ID = "quick-actions";
const CATEGORY_WEIGHTS = new Map<string, number>([
[DEFAULT_CATEGORY_ID, 100],
[CONTEXT_PROVIDER_ID, 95],
[SESSION_TOOLS_CATEGORY_ID, 90],
["discord-settings", 90],
["updates", 75],
[CUSTOM_COMMANDS_CATEGORY_ID, 80],
["plugins", 45],
["plugins-enable", 45],
["plugins-disable", 45],
["plugins-settings", 40],
["plugins-changes", 40],
[TOOLBOX_ACTIONS_CATEGORY_ID, 45],
[CHATBAR_ACTIONS_CATEGORY_ID, 45],
[GUILD_CATEGORY_ID, 40],
[FRIENDS_CATEGORY_ID, 40]
]);
const CATEGORY_GROUP_LABELS = new Map<string | undefined, string>([
[DEFAULT_CATEGORY_ID, "Core Actions"],
[CONTEXT_PROVIDER_ID, "Core Actions"],
[SESSION_TOOLS_CATEGORY_ID, "Core Actions"],
["discord-settings", "Discord Settings"],
["updates", "Updates"],
[CUSTOM_COMMANDS_CATEGORY_ID, "Custom Commands"],
["plugins", "Plugin Controls"],
["plugins-enable", "Plugin Controls"],
["plugins-disable", "Plugin Controls"],
["plugins-settings", "Plugin Controls"],
["plugins-changes", "Plugin Controls"],
[TOOLBOX_ACTIONS_CATEGORY_ID, "Plugin Controls"],
[CHATBAR_ACTIONS_CATEGORY_ID, "Plugin Controls"],
[GUILD_CATEGORY_ID, "Guilds"],
[FRIENDS_CATEGORY_ID, "Friends"]
]);
const DEFAULT_CATEGORY_WEIGHT = 50;
const CATEGORY_DEFAULT_TAGS = new Map<string, string[]>([
[DEFAULT_CATEGORY_ID, [TAG_CORE]],
[CONTEXT_PROVIDER_ID, [TAG_CONTEXT]],
[SESSION_TOOLS_CATEGORY_ID, [TAG_SESSION]],
["discord-settings", [TAG_NAVIGATION]],
["updates", [TAG_DEVELOPER]],
[CUSTOM_COMMANDS_CATEGORY_ID, [TAG_CUSTOM]],
["plugins", [TAG_PLUGINS]],
["plugins-enable", [TAG_PLUGINS]],
["plugins-disable", [TAG_PLUGINS]],
["plugins-settings", [TAG_PLUGINS]],
["plugins-changes", [TAG_PLUGINS]],
[TOOLBOX_ACTIONS_CATEGORY_ID, [TAG_PLUGINS, TAG_UTILITY]],
[CHATBAR_ACTIONS_CATEGORY_ID, [TAG_PLUGINS, TAG_UTILITY]],
[GUILD_CATEGORY_ID, [TAG_GUILDS]],
[FRIENDS_CATEGORY_ID, [TAG_FRIENDS]]
]);
const PINNED_STORAGE_KEY = "CommandPalettePinned";
const pinnedCommandIds = new Set<string>();
const pinListeners = new Set<(pins: Set<string>) => void>();
interface ContextCommandProvider {
id: string;
getCommands(): CommandEntry[];
subscribe?(refresh: () => void): () => void;
}
interface ContextProviderRecord {
provider: ContextCommandProvider;
commandIds: Set<string>;
unsubscribe?: () => void;
}
const contextProviders = new Map<string, ContextProviderRecord>();
let pluginToggleSettingsUnsubscribe: (() => void) | null = null;
type CustomCommandAction =
| { type: "command"; commandId: string; }
| { type: "settings"; route: string; }
| { type: "url"; url: string; openExternal?: boolean; }
| { type: "macro"; steps: string[]; };
export interface CustomCommandDefinition {
id: string;
label: string;
description?: string;
keywords?: string[];
tags?: string[];
categoryId?: string;
danger?: boolean;
action: CustomCommandAction;
}
let customCommands: CustomCommandDefinition[] = [];
const customCommandListeners = new Set<(commands: CustomCommandDefinition[]) => void>();
const customCommandsReady = (async () => {
try {
const stored = await DataStore.get<CustomCommandDefinition[]>(CUSTOM_COMMANDS_KEY);
if (Array.isArray(stored)) {
setCustomCommands(stored);
}
} catch (error) {
console.error("CommandPalette", "Failed to load custom commands", error);
}
})();
function generateCustomId(existing: Set<string>) {
let id: string;
do {
id = `custom-${Math.random().toString(36).slice(2, 8)}`;
} while (existing.has(id));
return id;
}
function sanitizeCustomCommands(input: CustomCommandDefinition[] | undefined | null): CustomCommandDefinition[] {
if (!Array.isArray(input)) return [];
const seen = new Set<string>();
const result: CustomCommandDefinition[] = [];
for (const entry of input) {
if (!entry || typeof entry !== "object") continue;
const normalizedAction = (() => {
const action = entry.action as CustomCommandAction | undefined;
if (!action || typeof action !== "object" || typeof action.type !== "string") return null;
switch (action.type) {
case "command":
return { type: "command", commandId: String(action.commandId ?? "") } as CustomCommandAction;
case "settings":
return { type: "settings", route: String(action.route ?? "") } as CustomCommandAction;
case "url":
return { type: "url", url: String(action.url ?? ""), openExternal: Boolean(action.openExternal) } as CustomCommandAction;
case "macro":
if (!Array.isArray((action as any).steps)) return { type: "macro", steps: [] } as CustomCommandAction;
return {
type: "macro",
steps: (action as any).steps.map((step: unknown) => String(step)).filter(Boolean)
} as CustomCommandAction;
default:
return null;
}
})();
if (!normalizedAction) continue;
const keywords = Array.isArray(entry.keywords)
? entry.keywords.map(keyword => String(keyword).trim()).filter(Boolean)
: [];
const tags: string[] = [];
if (Array.isArray(entry.tags)) {
const seenTags = new Set<string>();
for (const rawTag of entry.tags) {
const trimmedTag = String(rawTag ?? "").trim();
if (!trimmedTag) continue;
const tagSlug = normalizeTag(trimmedTag);
if (!tagSlug || seenTags.has(tagSlug)) continue;
seenTags.add(tagSlug);
tags.push(trimmedTag);
}
}
let id = typeof entry.id === "string" && entry.id.trim().length ? entry.id.trim() : "";
if (!id || seen.has(id)) {
id = generateCustomId(seen);
}
const label = typeof entry.label === "string" ? entry.label.trim() : "";
const normalized: CustomCommandDefinition = {
id,
label,
description: entry.description ? String(entry.description) : undefined,
keywords,
tags,
categoryId: entry.categoryId ? String(entry.categoryId) : undefined,
danger: Boolean(entry.danger),
action: normalizedAction
};
seen.add(id);
result.push(normalized);
}
return result;
}
function emitCustomCommands() {
const snapshot = customCommands.map(command => ({
...command,
keywords: command.keywords ? [...command.keywords] : undefined,
tags: command.tags ? [...command.tags] : undefined
}));
for (const listener of customCommandListeners) listener(snapshot);
}
function setCustomCommands(commands: CustomCommandDefinition[]) {
customCommands = sanitizeCustomCommands(commands);
emitCustomCommands();
refreshContextProvider(CUSTOM_PROVIDER_ID);
}
export function getCustomCommandsSnapshot(): CustomCommandDefinition[] {
return customCommands.map(command => ({
...command,
keywords: command.keywords ? [...command.keywords] : undefined,
tags: command.tags ? [...command.tags] : undefined
}));
}
export function subscribeCustomCommands(listener: (commands: CustomCommandDefinition[]) => void) {
customCommandListeners.add(listener);
listener(getCustomCommandsSnapshot());
return () => {
customCommandListeners.delete(listener);
};
}
export async function saveCustomCommands(commands: CustomCommandDefinition[]) {
setCustomCommands(commands);
try {
await DataStore.set(CUSTOM_COMMANDS_KEY, customCommands);
} catch (error) {
console.error("CommandPalette", "Failed to persist custom commands", error);
}
}
function getStatusLabel(status: string) {
switch (status) {
case "online":
return "Online";
case "idle":
return "Idle";
case "dnd":
return "Do Not Disturb";
case "invisible":
return "Invisible";
default:
return status;
}
}
function clearScheduledStatusReset(showToastMessage = true) {
if (statusResetTimeout != null) {
clearTimeout(statusResetTimeout);
statusResetTimeout = null;
if (showToastMessage) {
showToast("Canceled scheduled status reset.", Toasts.Type.MESSAGE);
}
}
statusResetPrevious = null;
}
function scheduleStatusReset(durationMinutes: number) {
if (!StatusSetting) {
showToast("Unable to change status right now.", Toasts.Type.FAILURE);
return;
}
const previous = StatusSetting.getSetting?.() ?? "online";
clearScheduledStatusReset(false);
StatusSetting.updateSetting("dnd");
statusResetPrevious = previous;
statusResetTimeout = window.setTimeout(() => {
const target = statusResetPrevious ?? "online";
StatusSetting.updateSetting(target);
showToast(`Status reverted to ${getStatusLabel(target)}.`, Toasts.Type.SUCCESS);
statusResetTimeout = null;
statusResetPrevious = null;
}, durationMinutes * 60_000);
showToast(`Do Not Disturb for ${durationMinutes} minutes.`, Toasts.Type.SUCCESS);
}
function ensureSessionHooks() {
if (sessionHooksInitialized) return;
sessionHooksInitialized = true;
patchChannelCloseMethods();
const handleChannelClose = (payload: any) => {
const channel = resolveChannelFromPayload(payload);
if (channel) trackClosedChannel(channel);
const snapshot = lastClosedDm;
if (!snapshot || !snapshot.recipientId) {
const userId = extractUserId(payload);
if (userId) {
const fallbackChannelId = snapshot?.channelId ?? (typeof channel?.id === "string" ? channel.id : null);
trackClosedRecipient(userId, fallbackChannelId);
}
}
};
FluxDispatcher.subscribe?.("CHANNEL_CLOSE", handleChannelClose);
sessionCleanupCallbacks.push(() => {
FluxDispatcher.unsubscribe?.("CHANNEL_CLOSE", handleChannelClose);
sessionHooksInitialized = false;
lastClosedDm = null;
});
}
export function cleanupCommandPaletteRuntime() {
clearScheduledStatusReset(false);
while (sessionCleanupCallbacks.length) {
const callback = sessionCleanupCallbacks.pop();
try {
callback?.();
} catch (error) {
console.error("CommandPalette", "Failed to cleanup session callback", error);
}
}
while (runtimeCleanupCallbacks.length) {
const callback = runtimeCleanupCallbacks.pop();
try {
callback?.();
} catch (error) {
console.error("CommandPalette", "Failed to cleanup runtime callback", error);
}
}
for (const record of contextProviders.values()) {
try {
record.unsubscribe?.();
} catch (error) {
console.error("CommandPalette", "Failed to unsubscribe context provider", record.provider.id, error);
}
record.unsubscribe = undefined;
record.commandIds.clear();
}
lastClosedDm = null;
contextProviders.clear();
categories.clear();
registry.clear();
cachedSortedCommands = null;
categoryCommandCache.clear();
treeCommandCache.clear();
searchTextCache.clear();
commandTagIds.clear();
tagMetadata.clear();
recentCommandIds.length = 0;
builtInsRegistered = false;
registryVersion += 1;
emitRegistryVersion();
registryListeners.clear();
}
export function getCategoryWeight(categoryId?: string): number {
if (!categoryId) return DEFAULT_CATEGORY_WEIGHT;
return CATEGORY_WEIGHTS.get(categoryId) ?? DEFAULT_CATEGORY_WEIGHT;
}
export function getCategoryGroupLabel(categoryId?: string): string {
return CATEGORY_GROUP_LABELS.get(categoryId) ?? "Other";
}
export function getRecentRank(commandId: string): number {
return recentCommandIds.indexOf(commandId);
}
function refreshContextProvider(id: string) {
const record = contextProviders.get(id);
if (!record) return;
const { commandIds } = record;
const previousIds = Array.from(commandIds);
const preservedPins = new Set<string>();
if (previousIds.length) {
for (const commandId of previousIds) {
const preservePin = pinnedCommandIds.has(commandId);
if (preservePin) preservedPins.add(commandId);
removeCommand(commandId, preservePin ? { preservePin: true } : undefined);
commandIds.delete(commandId);
}
}
let commands: CommandEntry[] = [];
try {
commands = record.provider.getCommands() ?? [];
} catch (error) {
console.error("CommandPalette", "Failed to compute commands for provider", record.provider.id, error);
commands = [];
}
const nextIds = new Set<string>();
for (const entry of commands) {
registerCommand(entry);
commandIds.add(entry.id);
nextIds.add(entry.id);
}
if (preservedPins.size) {
let changed = false;
for (const commandId of preservedPins) {
if (!nextIds.has(commandId) && pinnedCommandIds.delete(commandId)) {
changed = true;
}
}
if (changed) emitPinned();
}
}
function scheduleProviderRegistration(record: ContextProviderRecord) {
if (!record.provider.subscribe) return;
const refresh = () => refreshContextProvider(record.provider.id);
const unsubscribe = record.provider.subscribe(refresh);
record.unsubscribe = unsubscribe;
}
function registerContextProvider(provider: ContextCommandProvider) {
if (contextProviders.has(provider.id)) return;
const record: ContextProviderRecord = {
provider,
commandIds: new Set<string>()
};
contextProviders.set(provider.id, record);
scheduleProviderRegistration(record);
refreshContextProvider(provider.id);
}
export function refreshAllContextProviders() {
for (const id of contextProviders.keys()) {
refreshContextProvider(id);
}
}
type RemoveCommandOptions = {
preservePin?: boolean;
};
function removeCommand(commandId: string, options?: RemoveCommandOptions) {
const existing = registry.get(commandId);
if (!existing) return;
const wasPinned = pinnedCommandIds.has(commandId);
registry.delete(commandId);
if (wasPinned && !options?.preservePin) {
pinnedCommandIds.delete(commandId);
emitPinned();
}
searchTextCache.delete(commandId);
const tagIds = commandTagIds.get(commandId);
if (tagIds) {
for (const tagId of tagIds) {
decrementTagMetadata(tagId);
}
commandTagIds.delete(commandId);
}
bumpRegistryVersion();
}
export function getCommandById(commandId: string): CommandEntry | undefined {
return registry.get(commandId);
}
const pinsReady = (async () => {
try {
const stored = await DataStore.get<string[]>(PINNED_STORAGE_KEY);
if (Array.isArray(stored)) {
for (const id of stored) pinnedCommandIds.add(id);
}
} catch (error) {
console.error("CommandPalette", "Failed to load pinned commands", error);
}
})();
function invalidateCaches() {
cachedSortedCommands = null;
categoryCommandCache.clear();
treeCommandCache.clear();
}
function emitRegistryVersion() {
for (const listener of registryListeners) listener(registryVersion);
}
function bumpRegistryVersion() {
registryVersion += 1;
invalidateCaches();
emitRegistryVersion();
}
function computeSearchText(entry: CommandEntry) {
const keywords = entry.keywords ?? [];
const tags = entry.tags ?? [];
return [entry.label, entry.description ?? "", entry.shortcut ?? "", ...keywords, ...tags]
.filter(Boolean)
.join(" ")
.toLowerCase();
}
function recordRecent(commandId: string) {
const index = recentCommandIds.indexOf(commandId);
if (index !== -1) recentCommandIds.splice(index, 1);
recentCommandIds.unshift(commandId);
if (recentCommandIds.length > RECENT_COMMAND_LIMIT) {
recentCommandIds.length = RECENT_COMMAND_LIMIT;
}
}
export function getRecentCommands(): CommandEntry[] {
const results: CommandEntry[] = [];
for (const id of recentCommandIds) {
const entry = registry.get(id);
if (entry) results.push(entry);
}
return results;
}
function getNewestRecentCommand(excludeId?: string): CommandEntry | undefined {
for (const id of recentCommandIds) {
if (id === excludeId || RECENT_COMMAND_SKIP_IDS.has(id)) continue;
const entry = registry.get(id);
if (entry) return entry;
}
return undefined;
}
function emitPinned() {
const snapshot = new Set(pinnedCommandIds);
for (const listener of pinListeners) listener(snapshot);
}
async function persistPinned() {
try {
await DataStore.set(PINNED_STORAGE_KEY, Array.from(pinnedCommandIds));
} catch (error) {
console.error("CommandPalette", "Failed to save pinned commands", error);
}
}
function prunePinned() {
let changed = false;
for (const id of Array.from(pinnedCommandIds)) {
if (!registry.has(id)) {
pinnedCommandIds.delete(id);
changed = true;
}
}
return changed;
}
pinsReady.then(() => emitPinned());
const BUILT_IN_CATEGORIES: CommandCategory[] = [
{
id: DEFAULT_CATEGORY_ID,
label: "Quick Actions",
description: "Common Equicord shortcuts"
},
{
id: "plugins",
label: "Plugins",
description: "Manage Equicord and Vencord plugins"
},
{
id: CONTEXT_PROVIDER_ID,
label: "Current Context",
description: "Actions for the selected channel and guild"
},
{
id: "plugins-enable",
label: "Enable Plugin",
parentId: "plugins"
},
{
id: "plugins-disable",
label: "Disable Plugin",
parentId: "plugins"
},
{
id: "plugins-settings",
label: "Plugin Settings",
parentId: "plugins"
},
{
id: TOOLBOX_ACTIONS_CATEGORY_ID,
label: "Plugin Actions",
parentId: "plugins"
},
{
id: CHATBAR_ACTIONS_CATEGORY_ID,
label: "Chat Bar Buttons",
parentId: "plugins"
},
{
id: "plugins-changes",
label: "Plugin Changes",
parentId: "plugins"
},
{
id: "updates",
label: "Updates",
description: "Stay up to date with Equicord"
},
{
id: "discord-settings",
label: "Discord Settings",
description: "Jump to Discord configuration pages"
},
{
id: CUSTOM_COMMANDS_CATEGORY_ID,
label: "Custom Commands",
description: "User-defined command palette entries"
},
{
id: SESSION_TOOLS_CATEGORY_ID,
label: "Session Tools",
description: "Utilities for managing your Discord session"
},
{
id: GUILD_CATEGORY_ID,
label: "Guilds",
description: "Quickly navigate to your guilds"
},
{
id: FRIENDS_CATEGORY_ID,
label: "Friends",
description: "Quickly DM your friends"
}
];
const BUILT_IN_COMMANDS: CommandEntry[] = [
{
id: "open-equicord-settings",
label: "Open Equicord Settings",
keywords: ["settings", "equicord"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_NAVIGATION, TAG_CORE],
handler: () => openUserSettingsPanel("equicord_main")
},
{
id: "open-plugin-settings",
label: "Open Plugin Settings",
keywords: ["settings", "plugins"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_NAVIGATION, TAG_PLUGINS],
handler: () => openUserSettingsPanel("equicord_plugins")
},
{
id: "reload-windows",
label: "Reload Discord",
description: "Reloads the current Discord window",
keywords: ["reload"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_DEVELOPER, TAG_UTILITY],
handler: () => window.location.reload()
}
];
const DISCORD_SETTINGS_COMMANDS: Array<{ id: string; label: string; route: string; keywords: string[]; description?: string; }> = [
{ id: "settings-account", label: "Open My Account", route: "my_account", keywords: ["account", "profile"] },
{ id: "settings-privacy", label: "Open Data & Privacy", route: "data_and_privacy", keywords: ["privacy", "safety", "data"] },
{ id: "settings-notifications", label: "Open Notifications", route: "legacy_notifications_settings", keywords: ["notifications"] },
{ id: "settings-voice", label: "Open Voice & Video", route: "voice_and_video", keywords: ["voice", "video", "audio"] },
{ id: "settings-text", label: "Open Text & Images", route: "text_and_images", keywords: ["text", "images"] },
{ id: "settings-appearance", label: "Open Appearance", route: "appearance", keywords: ["appearance", "theme"] },
{ id: "settings-accessibility", label: "Open Accessibility", route: "accessibility", keywords: ["accessibility"] },
{ id: "settings-keybinds", label: "Open Keybinds", route: "keybinds", keywords: ["keybinds", "shortcuts"] },
{ id: "settings-advanced", label: "Open Advanced", route: "advanced", keywords: ["advanced"] }
];
const settingsCommandsById = new Map<string, typeof DISCORD_SETTINGS_COMMANDS[number]>();
const settingsCommandsByRoute = new Map<string, typeof DISCORD_SETTINGS_COMMANDS[number]>();
let builtInsRegistered = false;
export function registerCategory(category: CommandCategory) {
const parentId = category.parentId ?? null;
const normalized = { ...category, parentId } satisfies CommandCategory;
const existing = categories.get(category.id);
if (
existing &&
existing.label === normalized.label &&
existing.description === normalized.description &&
(existing.parentId ?? null) === parentId
) {
return;
}
categories.set(category.id, normalized);
bumpRegistryVersion();
}
export function listCategories(): CommandCategory[] {
return Array.from(categories.values()).sort((a, b) => a.label.localeCompare(b.label));
}
export function listChildCategories(parentId: string | null): CommandCategory[] {
const normalized = parentId ?? null;
return listCategories().filter(cat => (cat.parentId ?? null) === normalized);
}
export function getCategoryPath(categoryId?: string): CommandCategory[] {
if (!categoryId) return [];
const path: CommandCategory[] = [];
let current: CommandCategory | undefined = categories.get(categoryId);
while (current) {
path.unshift(current);
current = current.parentId ? categories.get(current.parentId) : undefined;
}
return path;
}
export function registerCommand(entry: CommandEntry) {
const preservePin = pinnedCommandIds.has(entry.id);
if (registry.has(entry.id)) {
removeCommand(entry.id, preservePin ? { preservePin: true } : undefined);
}
const categoryId = entry.categoryId ?? DEFAULT_CATEGORY_ID;
if (!categories.has(categoryId)) {
registerCategory({
id: categoryId,
label: categoryId
.split(/[\s-]+/)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(" "),
parentId: null
});
}
const labelByTagId = new Map<string, string>();
const addTag = (rawTag: string | undefined | null) => {
if (!rawTag) return;
const normalizedTag = normalizeTag(rawTag);
if (!normalizedTag) return;
if (!labelByTagId.has(normalizedTag)) {
labelByTagId.set(normalizedTag, rawTag.trim());
}
};
if (Array.isArray(entry.tags)) {
for (const tag of entry.tags) addTag(tag);
}
const defaultTags = CATEGORY_DEFAULT_TAGS.get(categoryId);
if (defaultTags) {
for (const tag of defaultTags) addTag(tag);
}
const normalizedTagIds = Array.from(labelByTagId.keys());
const displayTags = Array.from(labelByTagId.values());
const normalized: CommandEntry = {
...entry,
categoryId,
hiddenInSearch: entry.hiddenInSearch ?? false,
searchGroup: entry.searchGroup,
tags: displayTags
};
registry.set(entry.id, normalized);
commandTagIds.set(entry.id, normalizedTagIds);
for (const tagId of normalizedTagIds) {
const label = labelByTagId.get(tagId) ?? tagId;
incrementTagMetadata(tagId, label);
}
searchTextCache.set(entry.id, computeSearchText(normalized));
bumpRegistryVersion();
}
export function listCommands(): CommandEntry[] {
if (!cachedSortedCommands) {
cachedSortedCommands = Array.from(registry.values()).sort((a, b) => a.label.localeCompare(b.label));
}
return cachedSortedCommands;
}
export function listCommandsByCategory(categoryId: string): CommandEntry[] {
const cached = categoryCommandCache.get(categoryId);
if (cached) return cached;
const filtered = listCommands().filter(entry => entry.categoryId === categoryId);
categoryCommandCache.set(categoryId, filtered);
return filtered;
}
export function isCommandPinned(commandId: string): boolean {
return pinnedCommandIds.has(commandId);
}
export async function togglePinned(commandId: string): Promise<boolean | null> {
await pinsReady;
if (!commandId) return null;
if (!registry.has(commandId)) {
refreshAllContextProviders();
}
if (!registry.has(commandId)) {
console.warn("CommandPalette", "Attempted to pin unavailable command", commandId);
return null;
}
const willPin = !pinnedCommandIds.has(commandId);
if (willPin) {
pinnedCommandIds.add(commandId);
} else {
pinnedCommandIds.delete(commandId);
}
await persistPinned();
emitPinned();
return willPin;
}
export function subscribePinned(listener: (pins: Set<string>) => void) {
let active = true;
const wrapped = (pins: Set<string>) => {
if (!active) return;
listener(new Set(pins));
};
pinListeners.add(wrapped);
void pinsReady.then(() => wrapped(pinnedCommandIds));
return () => {
active = false;
pinListeners.delete(wrapped);
};
}
function collectCategoryTreeIds(rootId: string): Set<string> {
const visited = new Set<string>();
const queue: string[] = [rootId];
while (queue.length) {
const id = queue.shift()!;
if (visited.has(id)) continue;
visited.add(id);
for (const category of categories.values()) {
const parent = category.parentId ?? null;
if (parent === id) {
queue.push(category.id);
}
}
}
return visited;
}
export function listCommandsInTree(categoryId: string): CommandEntry[] {
const cached = treeCommandCache.get(categoryId);
if (cached) return cached;
const categoryIds = collectCategoryTreeIds(categoryId);
const filtered = listCommands().filter(entry => entry.categoryId && categoryIds.has(entry.categoryId));
treeCommandCache.set(categoryId, filtered);
return filtered;
}
export function getCommandSearchText(commandId: string): string {
return searchTextCache.get(commandId) ?? "";
}
export function getRegistryVersion(): number {
return registryVersion;
}
export function listAllTags(): CommandTagMeta[] {
return Array.from(tagMetadata.entries())
.map(([id, meta]) => ({ id, label: meta.label, count: meta.count }))
.sort((a, b) => {
if (b.count === a.count) return a.label.localeCompare(b.label);
return b.count - a.count;
});
}
export function getCommandTagIds(commandId: string): string[] {
return commandTagIds.get(commandId) ?? [];
}
export function subscribeRegistry(listener: (version: number) => void) {
let active = true;
const wrapped = (version: number) => {
if (!active) return;
listener(version);
};
registryListeners.add(wrapped);
wrapped(registryVersion);
return () => {
active = false;
registryListeners.delete(wrapped);
};
}
export async function executeCommand(entry: CommandEntry) {
try {
await entry.handler();
recordRecent(entry.id);
} catch (error) {
Toasts.show({
message: `Command failed: ${entry.label}`,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
});
console.error("CommandPalette", error);
}
}
function showToast(message: string, type: ToastKind) {
Toasts.show({
message,
type,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
function createPluginKeywords(plugin: Plugin): string[] {
const base = [plugin.name.toLowerCase(), "plugin"];
if (plugin.description) {
base.push(...plugin.description.toLowerCase().split(/\s+/));
}
return Array.from(new Set(base.filter(Boolean)));
}
function buildPluginToggleCommand(plugin: Plugin): CommandEntry {
const enabled = isPluginEnabled(plugin.name);
const keywords = Array.from(new Set([
...createPluginKeywords(plugin),
"settings",
"configure",
"preferences"
]));
return {
id: `toggle-plugin-${plugin.name.toLowerCase()}`,
label: `${enabled ? "Disable" : "Enable"} ${plugin.name}`,
description: plugin.description,
keywords,
categoryId: enabled ? "plugins-disable" : "plugins-enable",
searchGroup: `plugin-${plugin.name.toLowerCase()}`,
tags: [TAG_PLUGINS, TAG_UTILITY],
handler: async () => {
const before = isPluginEnabled(plugin.name);
const result = await toggleEnabled(plugin.name);
const after = isPluginEnabled(plugin.name);
if (result && before !== after) {
showToast(`${after ? "Enabled" : "Disabled"} ${plugin.name}.`, Toasts.Type.SUCCESS);
} else if (!result) {
showToast(`Failed to toggle ${plugin.name}.`, Toasts.Type.FAILURE);
}
refreshPluginToggleCommand(plugin);
}
} satisfies CommandEntry;
}
function refreshPluginToggleCommand(plugin: Plugin) {
registerCommand(buildPluginToggleCommand(plugin));
}
function registerPluginToggleCommands() {
const { plugins } = Vencord.Plugins;
const pluginMap = plugins as Record<string, Plugin>;
for (const plugin of Object.values(pluginMap)) {
refreshPluginToggleCommand(plugin);
}
if (!pluginToggleSettingsUnsubscribe) {
const listener = (_: unknown, path: string) => {
if (typeof path !== "string" || !path.startsWith("plugins.")) return;
if (!path.endsWith(".enabled")) return;
const pluginName = path.slice("plugins.".length, -".enabled".length);
const currentPlugins = Vencord.Plugins.plugins as Record<string, Plugin>;
const target = currentPlugins[pluginName];
if (!target) return;
refreshPluginToggleCommand(target);
};
SettingsStore.addGlobalChangeListener(listener);
pluginToggleSettingsUnsubscribe = () => {
SettingsStore.removeGlobalChangeListener(listener);
};
runtimeCleanupCallbacks.push(() => {
pluginToggleSettingsUnsubscribe?.();
pluginToggleSettingsUnsubscribe = null;
});
}
}
function registerPluginSettingsCommands() {
const { plugins } = Vencord.Plugins;
const pluginMap = plugins as Record<string, Plugin>;
for (const plugin of Object.values(pluginMap)) {
const keywords = createPluginKeywords(plugin);
registerCommand({
id: `settings-plugin-${plugin.name.toLowerCase()}`,
label: `${plugin.name} Settings`,
description: plugin.description,
keywords,
categoryId: "plugins-settings",
handler: () => openPluginModal(plugin),
tags: [TAG_PLUGINS, TAG_NAVIGATION],
searchGroup: `plugin-${plugin.name.toLowerCase()}`
});
}
}
function registerUpdateCommands() {
registerCommand({
id: "check-for-updates",
label: "Check for Updates",
description: "Checks for Equicord updates",
keywords: ["updates", "check", "updater"],
categoryId: "updates",
tags: [TAG_DEVELOPER, TAG_UTILITY],
handler: async () => {
try {
const outdated = await checkForUpdates();
if (outdated) {
const count = changes.length;
showToast(count === 1 ? "One update available." : `${count} updates available.`, Toasts.Type.SUCCESS);
} else {
showToast("No updates found.", Toasts.Type.MESSAGE);
}
} catch (error) {
showToast("Failed to check for updates.", Toasts.Type.FAILURE);
console.error("CommandPalette", error);
}
}
});
registerCommand({
id: "open-changelog",
label: "View Changelog",
description: "Opens the Equicord changelog",
keywords: ["updates", "changelog"],
categoryId: "updates",
tags: [TAG_DEVELOPER, TAG_NAVIGATION],
handler: () => openUserSettingsPanel("equicord_changelog")
});
}
function registerDiscordSettingsCommands() {
for (const command of DISCORD_SETTINGS_COMMANDS) {
settingsCommandsById.set(command.id, command);
settingsCommandsByRoute.set(command.route, command);
if (command.id === "settings-privacy") {
settingsCommandsByRoute.set("Privacy & Safety", command);
}
registerCommand({
id: command.id,
label: command.label,
description: command.description,
keywords: command.keywords,
categoryId: "discord-settings",
tags: [TAG_NAVIGATION],
handler: async () => {
const fallbackRoute = command.id === "settings-privacy" ? "privacy_and_safety" : null;
try {
await openUserSettingsPanel(command.route);
} catch (error) {
if (fallbackRoute) {
await openUserSettingsPanel(fallbackRoute);
} else {
console.error("CommandPalette", "Failed to open Discord settings", command.route, error);
showToast(`Unable to open ${command.label}.`, Toasts.Type.FAILURE);
}
}
}
});
}
}
export function getSettingsCommandMetaById(id: string) {
return settingsCommandsById.get(id);
}
export function getSettingsCommandMetaByRoute(route: string) {
return settingsCommandsByRoute.get(route);
}
function getActiveChannel() {
const channelId = SelectedChannelStore?.getChannelId?.();
if (!channelId) return null;
return ChannelStore.getChannel(channelId) ?? null;
}
function getActiveGuildId(): string | null {
const guildId = SelectedGuildStore?.getGuildId?.();
return guildId ?? null;
}
function getChannelDisplayLabel(channel: any): string {
if (!channel) return "Current Channel";
try {
if (typeof channel.isDM === "function" && channel.isDM()) return "Direct Message";
if (typeof channel.isGroupDM === "function" && channel.isGroupDM()) return channel.name ?? "Group DM";
if (channel.name) return `#${channel.name}`;
if (channel.rawRecipients?.length) return "Direct Message";
} catch { }
return "Current Channel";
}
function getGuildDisplayLabel(guildId: string | null): string {
if (!guildId) return "Current Guild";
const guild = GuildStore.getGuild?.(guildId) ?? null;
return guild?.name ?? "Current Guild";
}
function buildMuteConfig(durationMinutes?: number | null) {
if (!durationMinutes || durationMinutes <= 0) return null;
const end = new Date(Date.now() + durationMinutes * 60_000).toISOString();
return {
end_time: end,
selected_time_window: durationMinutes
};
}
function toggleChannelMuteState(channelId: string, guildId: string | null, mute: boolean, durationMinutes?: number) {
const actions = NotificationSettingsActionCreators as any;
if (!actions?.updateChannelOverrideSettings) {
showToast("Channel mute controls unavailable.", Toasts.Type.FAILURE);
return;
}
const payload: Record<string, unknown> = {
muted: mute,
mute_config: mute ? buildMuteConfig(durationMinutes) : null
};
try {
actions.updateChannelOverrideSettings(guildId, channelId, payload);
showToast(`${mute ? "Muted" : "Unmuted"} channel.`, Toasts.Type.SUCCESS);
} catch (error) {
console.error("CommandPalette", "Failed to toggle channel mute", error);
showToast("Failed to update channel mute state.", Toasts.Type.FAILURE);
}
}
function toggleGuildMuteState(guildId: string, mute: boolean, durationMinutes?: number) {
const actions = NotificationSettingsActionCreators as any;
if (!actions?.updateGuildNotificationSettings) {
showToast("Guild mute controls unavailable.", Toasts.Type.FAILURE);
return;
}
const payload: Record<string, unknown> = {
muted: mute,
mute_config: mute ? buildMuteConfig(durationMinutes) : null
};
try {
actions.updateGuildNotificationSettings(guildId, payload);
showToast(`${mute ? "Muted" : "Unmuted"} server.`, Toasts.Type.SUCCESS);
} catch (error) {
console.error("CommandPalette", "Failed to toggle guild mute", error);
showToast("Failed to update server mute state.", Toasts.Type.FAILURE);
}
}
function minutesUntilTomorrow(): number {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
return Math.max(1, Math.ceil((tomorrow.getTime() - now.getTime()) / 60_000));
}
function openPinsForChannel(channelId: string) {
let dispatched = false;
if (ComponentDispatch?.dispatchToLastSubscribed) {
const toggleEvents = ["TOGGLE_CHANNEL_PINS", "CHANNEL_TOGGLE_PINNED_MESSAGES"];
for (const event of toggleEvents) {
try {
ComponentDispatch.dispatchToLastSubscribed(event, {
channelId,
open: true,
isOpen: true,
location: "commandPalette"
});
dispatched = true;
break;
} catch (error) {
console.error("CommandPalette", `Failed to dispatch ${event} for pins`, error);
}
}
}
if (dispatched) return;
try {
FluxDispatcher.dispatch({ type: "CHANNEL_PINS_MODAL_OPEN", channelId });
} catch (error) {
console.error("CommandPalette", "Failed to open pins", error);
showToast("Unable to open pins panel.", Toasts.Type.FAILURE);
}
}
function registerContextualCommands() {
registerContextProvider({
id: CONTEXT_PROVIDER_ID,
getCommands: () => createContextualCommands(),
subscribe: refresh => {
const handler = () => refresh();
SelectedChannelStore?.addChangeListener?.(handler);
SelectedGuildStore?.addChangeListener?.(handler);
UserGuildSettingsStore?.addChangeListener?.(handler);
return () => {
SelectedChannelStore?.removeChangeListener?.(handler);
SelectedGuildStore?.removeChangeListener?.(handler);
UserGuildSettingsStore?.removeChangeListener?.(handler);
};
}
});
}
function createContextualCommands(): CommandEntry[] {
const commands: CommandEntry[] = [];
const channel = getActiveChannel();
const guildId = getActiveGuildId();
const channelId = channel?.id as string | undefined;
if (channel && channelId) {
const channelLabel = getChannelDisplayLabel(channel);
commands.push({
id: "context-channel-mark-read",
label: `Mark ${channelLabel} Read`,
keywords: ["context", "mark", "read", channelLabel.replace(/#/g, "")],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_UTILITY],
handler: async () => {
try {
const ackChannel = ReadStateUtils?.ackChannel;
if (!ackChannel) throw new Error("ackChannel unavailable");
const ackTarget = ChannelStore.getChannel(channelId) ?? channel;
if (!ackTarget) throw new Error("Channel not found");
await Promise.resolve(ackChannel(ackTarget));
showToast(`Marked ${channelLabel} as read.`, Toasts.Type.SUCCESS);
} catch (error) {
console.error("CommandPalette", "Failed to mark read", error);
showToast("Failed to mark channel as read.", Toasts.Type.FAILURE);
}
}
});
commands.push({
id: "context-channel-open-pins",
label: `Open Pins for ${channelLabel}`,
keywords: ["context", "pins", channelLabel.replace(/#/g, "")],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_NAVIGATION],
handler: () => openPinsForChannel(channelId)
});
if (channel.guild_id && UserGuildSettingsStore?.isChannelMuted) {
const isMuted = UserGuildSettingsStore.isChannelMuted(channel.guild_id, channelId);
commands.push({
id: "context-channel-toggle-mute",
label: `${isMuted ? "Unmute" : "Mute"} ${channelLabel}`,
keywords: ["context", "mute", channelLabel.replace(/#/g, "")],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_UTILITY],
handler: () => toggleChannelMuteState(channelId, channel.guild_id, !isMuted)
});
}
}
if (guildId && guildId !== "@me" && UserGuildSettingsStore?.isMuted) {
const guildLabel = getGuildDisplayLabel(guildId);
const isGuildMuted = UserGuildSettingsStore.isMuted(guildId);
commands.push({
id: "context-guild-toggle-mute",
label: `${isGuildMuted ? "Unmute" : "Mute"} ${guildLabel}`,
keywords: ["context", "guild", "mute"],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_UTILITY],
handler: () => toggleGuildMuteState(guildId, !isGuildMuted)
});
if (!isGuildMuted) {
commands.push({
id: "context-guild-mute-hour",
label: `Mute ${guildLabel} for 1 Hour`,
keywords: ["context", "guild", "mute", "hour"],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_UTILITY],
handler: () => toggleGuildMuteState(guildId, true, 60)
});
commands.push({
id: "context-guild-mute-tomorrow",
label: `Mute ${guildLabel} Until Tomorrow`,
keywords: ["context", "guild", "mute", "tomorrow"],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_UTILITY],
handler: () => toggleGuildMuteState(guildId, true, minutesUntilTomorrow())
});
}
if (GuildSettingsActions?.open) {
commands.push({
id: "context-guild-open-settings",
label: `Open Settings for ${guildLabel}`,
keywords: ["context", "guild", "settings"],
categoryId: CONTEXT_PROVIDER_ID,
tags: [TAG_CONTEXT, TAG_NAVIGATION],
handler: async () => {
try {
await Promise.resolve(GuildSettingsActions.open(guildId, "OVERVIEW"));
} catch (error) {
console.error("CommandPalette", "Failed to open guild settings", error);
showToast("Unable to open guild settings.", Toasts.Type.FAILURE);
}
}
});
}
}
return commands;
}
async function runCommandById(commandId: string, visited: Set<string>) {
if (!commandId) return;
if (visited.has(commandId)) {
showToast("Command loop detected in macro execution.", Toasts.Type.FAILURE);
return;
}
if (commandId.startsWith("custom-")) {
visited.add(commandId);
}
const entry = getCommandById(commandId);
if (!entry) {
showToast(`Command ${commandId} not found.`, Toasts.Type.FAILURE);
return;
}
await executeCommand(entry);
}
async function runMacroSteps(steps: string[], visited: Set<string>) {
for (const step of steps) {
await runCommandById(step, visited);
}
}
async function executeCustomCommand(command: CustomCommandDefinition) {
const { action } = command;
try {
switch (action.type) {
case "command":
await runCommandById(action.commandId, new Set([command.id]));
break;
case "settings":
openUserSettingsPanel(action.route);
break;
case "url": {
const external = (window as any)?.DiscordNative?.app?.openExternalURL;
if (action.openExternal && typeof external === "function") {
external(action.url);
} else {
window.open(action.url, "_blank", "noopener,noreferrer");
}
break;
}
case "macro":
await runMacroSteps(action.steps, new Set([command.id]));
break;
default:
showToast("Unsupported custom command action.", Toasts.Type.FAILURE);
break;
}
} catch (error) {
console.error("CommandPalette", "Failed to execute custom command", command, error);
showToast(`Failed to run ${command.label}.`, Toasts.Type.FAILURE);
}
}
function createCustomCommandEntries(): CommandEntry[] {
return customCommands.map(command => ({
id: command.id,
label: command.label || "Untitled Command",
description: command.description,
keywords: command.keywords,
tags: command.tags,
categoryId: command.categoryId ?? CUSTOM_COMMANDS_CATEGORY_ID,
danger: command.danger,
handler: () => executeCustomCommand(command)
}));
}
function registerCustomCommandProvider() {
registerContextProvider({
id: CUSTOM_PROVIDER_ID,
getCommands: () => createCustomCommandEntries(),
subscribe: refresh => subscribeCustomCommands(() => refresh())
});
}
function clearDesktopNotifications() {
const api = (window as any)?.DiscordNative?.notifications;
if (api?.clearAll) {
try {
api.clearAll();
showToast("Cleared desktop notifications.", Toasts.Type.SUCCESS);
} catch (error) {
console.error("CommandPalette", "Failed to clear notifications", error);
showToast("Failed to clear notifications.", Toasts.Type.FAILURE);
}
} else {
showToast("Notification clearing not supported.", Toasts.Type.FAILURE);
}
}
function waitForDmChannel(userId: string, timeoutMs = 2000): Promise<string | null> {
return new Promise(resolve => {
const existing = ChannelStore.getDMFromUserId?.(userId) ?? null;
if (existing) {
resolve(existing);
return;
}
const addListener = ChannelStore.addChangeListener?.bind(ChannelStore);
const removeListener = ChannelStore.removeChangeListener?.bind(ChannelStore);
if (typeof addListener !== "function" || typeof removeListener !== "function") {
const timeoutId = window.setTimeout(() => {
resolve(ChannelStore.getDMFromUserId?.(userId) ?? null);
}, timeoutMs);
sessionCleanupCallbacks.push(() => window.clearTimeout(timeoutId));
return;
}
let settled = false;
let timeoutId: number | null = null;
const finish = (channelId: string | null) => {
if (settled) return;
settled = true;
if (timeoutId !== null) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
removeListener(listener);
resolve(channelId);
};
const listener = () => {
const channelId = ChannelStore.getDMFromUserId?.(userId) ?? null;
if (channelId) {
finish(channelId);
}
};
addListener(listener);
timeoutId = window.setTimeout(() => {
finish(ChannelStore.getDMFromUserId?.(userId) ?? null);
}, timeoutMs);
sessionCleanupCallbacks.push(() => {
if (!settled) {
settled = true;
if (timeoutId !== null) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
removeListener(listener);
resolve(ChannelStore.getDMFromUserId?.(userId) ?? null);
}
});
});
}
async function reopenLastClosedDm() {
ensureSessionHooks();
const snapshot = lastClosedDm;
if (!snapshot) {
showToast("No DM closures recorded this session.", Toasts.Type.MESSAGE);
return;
}
const { channelId, recipientId, isGroupDm } = snapshot;
const fallbackChannelId = typeof channelId === "string" ? channelId : null;
const navigate = (targetId: string | null | undefined): boolean => {
if (!targetId) return false;
ChannelRouter.transitionToChannel(targetId);
showToast("Reopened last closed DM.", Toasts.Type.SUCCESS);
return true;
};
try {
if (fallbackChannelId) {
const existingChannel = ChannelStore.getChannel(fallbackChannelId);
if (existingChannel && navigate(fallbackChannelId)) {
return;
}
}
if (!isGroupDm && recipientId && ChannelActionCreators?.openPrivateChannel) {
const immediateId = ChannelStore.getDMFromUserId?.(recipientId) ?? null;
if (navigate(immediateId)) {
return;
}
let reopenedId: string | null | undefined = null;
try {
const result = await Promise.resolve(ChannelActionCreators.openPrivateChannel({
recipientIds: [recipientId],
location: "CommandPalette",
navigateToChannel: false
}));
if (typeof result === "string") {
reopenedId = result;
}
} catch (error) {
console.error("CommandPalette", "Failed to open DM via ChannelActionCreators", error);
}
if (!reopenedId) {
reopenedId = await waitForDmChannel(recipientId, 2000);
}
if (navigate(reopenedId)) {
return;
}
const fallback = ChannelStore.getDMFromUserId?.(recipientId) ?? fallbackChannelId;
if (navigate(fallback)) {
return;
}
showToast("The DM could not be reopened.", Toasts.Type.FAILURE);
return;
}
if (navigate(fallbackChannelId)) {
return;
}
showToast("The DM is no longer available.", Toasts.Type.FAILURE);
} catch (error) {
console.error("CommandPalette", "Failed to reopen DM", error);
showToast("The DM is no longer available.", Toasts.Type.FAILURE);
} finally {
lastClosedDm = null;
}
}
function registerSessionCommands() {
ensureSessionHooks();
const commands: CommandEntry[] = [
{
id: "session-dnd-30",
label: "Set DND for 30 Minutes",
keywords: ["session", "status", "dnd", "30"],
categoryId: SESSION_TOOLS_CATEGORY_ID,
tags: [TAG_SESSION, TAG_UTILITY],
handler: () => scheduleStatusReset(30)
},
{
id: "session-dnd-60",
label: "Set DND for 1 Hour",
keywords: ["session", "status", "dnd", "60"],
categoryId: SESSION_TOOLS_CATEGORY_ID,
tags: [TAG_SESSION, TAG_UTILITY],
handler: () => scheduleStatusReset(60)
},
{
id: "session-status-cancel",
label: "Cancel Status Timer",
keywords: ["session", "status", "cancel"],
categoryId: SESSION_TOOLS_CATEGORY_ID,
tags: [TAG_SESSION, TAG_UTILITY],
handler: () => clearScheduledStatusReset()
},
{
id: "session-clear-notifications",
label: "Clear Desktop Notifications",
keywords: ["session", "notifications", "clear"],
categoryId: SESSION_TOOLS_CATEGORY_ID,
tags: [TAG_SESSION, TAG_UTILITY],
handler: clearDesktopNotifications
},
{
id: "session-reopen-dm",
label: "Reopen Last Closed DM",
keywords: ["session", "dm", "reopen"],
categoryId: SESSION_TOOLS_CATEGORY_ID,
tags: [TAG_SESSION, TAG_NAVIGATION],
handler: reopenLastClosedDm
}
];
for (const command of commands) {
registerCommand(command);
}
}
function registerGuildCommands() {
const guilds = GuildStore.getGuilds?.() ?? {};
Object.values(guilds).forEach((guild: Guild) => {
registerCommand({
id: `open-guild-${guild.id}`,
label: `Navigate to ${guild.name}`,
keywords: ["guild", "server", guild.name.toLowerCase()],
categoryId: GUILD_CATEGORY_ID,
tags: [TAG_GUILDS, TAG_NAVIGATION],
handler: () => {
NavigationRouter.transitionToGuild(guild.id);
}
} satisfies CommandEntry);
});
}
function registerFriendCommands() {
const friendIds = RelationshipStore.getFriendIDs();
friendIds.forEach((userId: string) => {
const user = UserStore.getUser(userId);
if (!user) return;
const displayName = RelationshipStore.getNickname(userId) || user.globalName || null;
const username = displayName
? `${displayName} (@${user.username})`
: user.username;
registerCommand({
id: `open-friend-${user.id}`,
label: `Open DM with ${username}`,
keywords: ["friend", "dm", username.toLowerCase()],
categoryId: FRIENDS_CATEGORY_ID,
tags: [TAG_FRIENDS, TAG_NAVIGATION],
handler: () => {
const channelId = ChannelStore.getDMFromUserId(user.id);
NavigationRouter.transitionTo(`/channels/@me/${channelId}`);
}
} satisfies CommandEntry);
});
}
function ensurePalettePlugin(): Plugin | null {
const plugin = Vencord.Plugins.plugins[COMMAND_PALETTE_PLUGIN_NAME] as Plugin | undefined;
if (!plugin) {
showToast("Command Palette plugin metadata unavailable.", Toasts.Type.FAILURE);
return null;
}
return plugin;
}
function setPresenceStatus(status: "online" | "idle" | "dnd" | "invisible") {
if (!StatusSetting) {
showToast("Unable to change status right now.", Toasts.Type.FAILURE);
return;
}
StatusSetting.updateSetting(status);
const label = {
online: "Online",
idle: "Idle",
dnd: "Do Not Disturb",
invisible: "Invisible"
}[status];
showToast(`Status set to ${label}.`, Toasts.Type.SUCCESS);
}
function toggleStreamerMode() {
const { enabled } = StreamerModeStore;
FluxDispatcher.dispatch({
type: "STREAMER_MODE_UPDATE",
key: "enabled",
value: !enabled
});
showToast(`Streamer Mode ${!enabled ? "enabled" : "disabled"}.`, Toasts.Type.SUCCESS);
}
function toggleSelfMute() {
VoiceActions.toggleSelfMute();
const muted = MediaEngineStore.isSelfMute();
showToast(muted ? "Microphone muted." : "Microphone unmuted.", Toasts.Type.SUCCESS);
}
function toggleSelfDeaf() {
VoiceActions.toggleSelfDeaf();
const deaf = MediaEngineStore.isSelfDeaf();
showToast(deaf ? "You are now deafened." : "You are no longer deafened.", Toasts.Type.SUCCESS);
}
function toggleQuickCss() {
Settings.useQuickCss = !Settings.useQuickCss;
showToast(`QuickCSS ${Settings.useQuickCss ? "enabled" : "disabled"}.`, Toasts.Type.SUCCESS);
}
function toggleWindowTransparency() {
Settings.transparent = !Settings.transparent;
showToast(`Window transparency ${Settings.transparent ? "enabled" : "disabled"}.`, Toasts.Type.SUCCESS);
}
function registerCommandPaletteUtilities() {
registerCommand({
id: "command-palette-open-settings",
label: "Open Command Palette Settings",
description: "Configure the Command Palette plugin",
keywords: ["command", "palette", "settings"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_NAVIGATION, TAG_DEVELOPER],
handler: () => {
const plugin = ensurePalettePlugin();
if (!plugin) return;
openPluginModal(plugin);
}
});
registerCommand({
id: "command-palette-show-recent",
label: "Show Recent Commands",
description: "Displays the last executed commands",
keywords: ["recent", "history"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_CORE],
handler: () => {
const recents = getRecentCommands().slice(0, 5);
if (recents.length === 0) {
showToast("No commands have been run yet.", Toasts.Type.MESSAGE);
return;
}
const message = recents
.map((entry, index) => `${index + 1}. ${entry.label}`)
.join("\n");
Toasts.show({
message,
type: Toasts.Type.MESSAGE,
id: Toasts.genId(),
options: { position: Toasts.Position.BOTTOM }
});
}
});
registerCommand({
id: "command-palette-rerun-last",
label: "Re-run Last Command",
keywords: ["recent", "again"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_CORE, TAG_UTILITY],
handler: async () => {
const last = getNewestRecentCommand("command-palette-rerun-last");
if (!last) {
showToast("No commands available to re-run.", Toasts.Type.MESSAGE);
return;
}
await executeCommand(last);
}
});
registerCommand({
id: "command-palette-toggle-pin-last",
label: "Toggle Pin on Last Command",
keywords: ["recent", "pin"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_CORE, TAG_UTILITY],
handler: async () => {
const last = getNewestRecentCommand("command-palette-toggle-pin-last");
if (!last) {
showToast("No recent commands to pin.", Toasts.Type.MESSAGE);
return;
}
const result = await togglePinned(last.id);
if (result === null) {
showToast(`${last.label} is not currently available to pin.`, Toasts.Type.MESSAGE);
return;
}
showToast(`${last.label} ${result ? "pinned" : "unpinned"}.`, Toasts.Type.SUCCESS);
}
});
}
function registerSystemUtilityCommands() {
const commands: Array<{ id: string; label: string; description?: string; handler: () => void; keywords: string[]; tags: string[]; }> = [
{
id: "set-status-online",
label: "Set Status: Online",
handler: () => setPresenceStatus("online"),
keywords: ["status", "online"],
tags: [TAG_CORE, TAG_UTILITY]
},
{
id: "set-status-idle",
label: "Set Status: Idle",
handler: () => setPresenceStatus("idle"),
keywords: ["status", "idle"],
tags: [TAG_CORE, TAG_UTILITY]
},
{
id: "set-status-dnd",
label: "Set Status: Do Not Disturb",
handler: () => setPresenceStatus("dnd"),
keywords: ["status", "dnd", "busy"],
tags: [TAG_CORE, TAG_UTILITY]
},
{
id: "set-status-invisible",
label: "Set Status: Invisible",
handler: () => setPresenceStatus("invisible"),
keywords: ["status", "offline", "invisible"],
tags: [TAG_CORE, TAG_UTILITY]
},
{
id: "toggle-streamer-mode",
label: "Toggle Streamer Mode",
handler: toggleStreamerMode,
keywords: ["stream", "privacy"],
tags: [TAG_UTILITY]
},
{
id: "toggle-self-mute",
label: "Toggle Self Mute",
handler: toggleSelfMute,
keywords: ["voice", "mute"],
tags: [TAG_UTILITY]
},
{
id: "toggle-self-deafen",
label: "Toggle Self Deafen",
handler: toggleSelfDeaf,
keywords: ["voice", "deafen"],
tags: [TAG_UTILITY]
}
];
for (const entry of commands) {
registerCommand({
id: entry.id,
label: entry.label,
description: entry.description,
keywords: entry.keywords,
categoryId: DEFAULT_CATEGORY_ID,
tags: entry.tags,
handler: entry.handler
});
}
}
function hotReloadPlugin(plugin: Plugin) {
if (!isPluginEnabled(plugin.name)) {
showToast(`${plugin.name} is disabled.`, Toasts.Type.MESSAGE);
return false;
}
if (plugin.patches?.length) {
showToast(`${plugin.name} requires a restart to reload.`, Toasts.Type.MESSAGE);
return false;
}
const stopped = Vencord.Plugins.stopPlugin(plugin);
if (!stopped) {
showToast(`Failed to stop ${plugin.name}.`, Toasts.Type.FAILURE);
return false;
}
const started = Vencord.Plugins.startPlugin(plugin);
if (!started) {
showToast(`Failed to restart ${plugin.name}.`, Toasts.Type.FAILURE);
return false;
}
showToast(`Reloaded ${plugin.name}.`, Toasts.Type.SUCCESS);
return true;
}
async function reloadAllPlugins() {
const entries = Object.values(Vencord.Plugins.plugins) as Plugin[];
let count = 0;
for (const plugin of entries) {
if (!isPluginEnabled(plugin.name)) continue;
if (hotReloadPlugin(plugin)) count += 1;
}
if (count === 0) {
showToast("No plugins were reloaded.", Toasts.Type.MESSAGE);
} else {
showToast(`Reloaded ${count} plugin${count === 1 ? "" : "s"}.`, Toasts.Type.SUCCESS);
}
}
async function setAllPluginsEnabled(enabled: boolean) {
const { plugins } = Vencord.Plugins;
let changed = 0;
for (const plugin of Object.values(plugins) as Plugin[]) {
const currentlyEnabled = isPluginEnabled(plugin.name);
if (enabled && currentlyEnabled) continue;
if (!enabled) {
if (isPluginEnabled(plugin.name)) continue;
if (!currentlyEnabled) continue;
}
if (plugin.patches?.length) continue;
const result = await toggleEnabled(plugin.name);
if (result) {
const nowEnabled = isPluginEnabled(plugin.name);
if (nowEnabled === enabled) changed += 1;
}
}
showToast(changed ? `${enabled ? "Enabled" : "Disabled"} ${changed} plugin${changed === 1 ? "" : "s"}.` : "No plugins changed state.", Toasts.Type.SUCCESS);
}
function registerPluginChangeCommands() {
registerCommand({
id: "reload-all-plugins",
label: "Reload All Plugins",
description: "Attempts to hot reload every enabled plugin",
keywords: ["plugin", "reload", "restart"],
categoryId: "plugins-changes",
tags: [TAG_PLUGINS, TAG_DEVELOPER, TAG_UTILITY],
handler: () => void reloadAllPlugins()
});
registerCommand({
id: "enable-all-plugins",
label: "Enable All Plugins",
keywords: ["plugin", "enable"],
categoryId: "plugins-changes",
tags: [TAG_PLUGINS, TAG_UTILITY],
handler: () => void setAllPluginsEnabled(true)
});
registerCommand({
id: "disable-all-plugins",
label: "Disable All Non-required Plugins",
keywords: ["plugin", "disable"],
categoryId: "plugins-changes",
tags: [TAG_PLUGINS, TAG_UTILITY],
handler: () => void setAllPluginsEnabled(false)
});
registerCommand({
id: "restart-equicord",
label: "Restart Equicord",
description: "Reloads the Discord client window",
keywords: ["restart", "reload"],
categoryId: "plugins-changes",
tags: [TAG_DEVELOPER, TAG_UTILITY],
handler: () => window.location.reload()
});
}
function slugifyActionLabel(label: string) {
return label
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function createPluginToolboxCommands(): CommandEntry[] {
const { plugins } = Vencord.Plugins;
const pluginMap = plugins as Record<string, Plugin>;
const entries: CommandEntry[] = [];
const usedIds = new Set<string>();
for (const plugin of Object.values(pluginMap)) {
const { toolboxActions } = plugin;
const actionEntries = Object.entries(toolboxActions ?? {});
if (actionEntries.length === 0) continue;
for (const [actionLabel, action] of actionEntries) {
if (typeof action !== "function") continue;
const normalizedLabel = actionLabel?.trim() || "Action";
const baseSlug = slugifyActionLabel(normalizedLabel) || "action";
const baseId = `plugin-action-${plugin.name.toLowerCase()}-${baseSlug}`;
let uniqueId = baseId;
let counter = 2;
while (usedIds.has(uniqueId) || registry.has(uniqueId)) {
uniqueId = `${baseId}-${counter++}`;
}
usedIds.add(uniqueId);
const enabled = isPluginEnabled(plugin.name);
const keywords = Array.from(new Set([
...createPluginKeywords(plugin),
...normalizedLabel.toLowerCase().split(/\s+/),
...(enabled ? [] : ["disabled"])
]));
entries.push({
id: uniqueId,
label: `${plugin.name}: ${normalizedLabel}`,
description: plugin.description,
keywords,
categoryId: TOOLBOX_ACTIONS_CATEGORY_ID,
tags: [TAG_PLUGINS, TAG_UTILITY, TAG_DEVELOPER],
handler: () => {
if (!isPluginEnabled(plugin.name)) {
showToast(`${plugin.name} is disabled. Enable the plugin to use this action.`, Toasts.Type.FAILURE);
return;
}
return runPluginToolboxAction(plugin, action, normalizedLabel);
}
});
}
}
return entries;
}
async function runPluginToolboxAction(plugin: Plugin, action: () => void | Promise<void>, actionLabel: string) {
try {
await Promise.resolve(action.call(plugin));
} catch (error) {
console.error("CommandPalette", "Toolbox action failed", plugin.name, actionLabel, error);
showToast(`Failed to run ${plugin.name}: ${actionLabel}`, Toasts.Type.FAILURE);
}
}
function registerPluginToolboxProvider() {
registerContextProvider({
id: TOOLBOX_ACTIONS_PROVIDER_ID,
getCommands: () => createPluginToolboxCommands(),
subscribe: refresh => {
const handler = (_: unknown, path: string) => {
if (typeof path !== "string" || !path.startsWith("plugins.")) return;
refresh();
};
SettingsStore.addGlobalChangeListener(handler);
return () => {
SettingsStore.removeGlobalChangeListener(handler);
};
}
});
}
function registerCustomizationCommands() {
registerCommand({
id: "toggle-quickcss",
label: "Toggle QuickCSS",
keywords: ["css", "theme", "quick"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_CUSTOMIZATION, TAG_UTILITY],
handler: toggleQuickCss
});
registerCommand({
id: "toggle-window-transparency",
label: "Toggle Window Transparency",
keywords: ["window", "transparency", "appearance"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_CUSTOMIZATION, TAG_UTILITY],
handler: toggleWindowTransparency
});
registerCommand({
id: "open-theme-library",
label: "Open Theme Library",
keywords: ["theme", "library"],
categoryId: DEFAULT_CATEGORY_ID,
tags: [TAG_CUSTOMIZATION, TAG_NAVIGATION],
handler: () => openUserSettingsPanel("equicord_themes")
});
}
export function registerBuiltInCommands() {
if (builtInsRegistered) return;
for (const category of BUILT_IN_CATEGORIES) {
registerCategory(category);
}
for (const entry of BUILT_IN_COMMANDS) {
registerCommand(entry);
}
registerCommandPaletteUtilities();
registerSystemUtilityCommands();
registerUpdateCommands();
registerDiscordSettingsCommands();
registerPluginToggleCommands();
registerPluginSettingsCommands();
registerPluginChangeCommands();
registerPluginToolboxProvider();
registerCustomizationCommands();
registerContextualCommands();
registerCustomCommandProvider();
registerSessionCommands();
registerGuildCommands();
registerFriendCommands();
void pinsReady.then(async () => {
if (prunePinned()) {
await persistPinned();
}
emitPinned();
});
builtInsRegistered = true;
}