import chardet from "chardet";
import dayjs from "dayjs";
import iconvLite from "iconv-lite";
import {Base64} from "js-base64";
import * as crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import Showdown from "showdown";
// import {Iconv} from "iconv"
import {isMac, isWin} from "./env";
import FileIndex from "../mapi/file";

export const EncodeUtil = {
    base32Alphabet: "abcdefghijklmnopqrstuvwxyz234567",
    base32Encode(str: string) {
        const buffer = Buffer.from(str, "utf8");
        let bits = "";
        let output = "";
        // 将每个字节转为8位二进制
        for (let i = 0; i < buffer.length; i++) {
            const byte = buffer[i];
            bits += byte.toString(2).padStart(8, "0");
        }
        // 每5位一组，转为 Base32 字符
        for (let i = 0; i < bits.length; i += 5) {
            const chunk = bits.slice(i, i + 5);
            const paddedChunk = chunk.padEnd(5, "0"); // 不足5位补0
            const index = parseInt(paddedChunk, 2);
            output += EncodeUtil.base32Alphabet[index];
        }
        return output;
    },
    base32Decode(str: string) {
        const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567";
        let bits = "";
        for (let i = 0; i < str.length; i++) {
            const char = str[i];
            const index = base32Alphabet.indexOf(char);
            if (index === -1) {
                throw new Error("Invalid Base32 character: " + char);
            }
            bits += index.toString(2).padStart(5, "0");
        }

        const bytes: number[] = [];
        for (let i = 0; i + 8 <= bits.length; i += 8) {
            const byte = bits.slice(i, i + 8);
            bytes.push(parseInt(byte, 2));
        }

        return Buffer.from(bytes).toString("utf8");
    },
    base64Encode(str: string) {
        return Base64.encode(str);
    },
    base64Decode(str: string) {
        return Base64.decode(str);
    },
    md5(str: string) {
        return crypto.createHash("md5").update(str).digest("hex");
    },
    aesEncode(str: string, key: string) {
        const cipher = crypto.createCipheriv("aes-128-ecb", key, "");
        let crypted = cipher.update(str, "utf8", "base64");
        crypted += cipher.final("base64");
        return crypted;
    },
    aesDecode(str: string, key: string) {
        const decipher = crypto.createDecipheriv("aes-128-ecb", key, "");
        let dec = decipher.update(str, "base64", "utf8");
        dec += decipher.final("utf8");
        return dec;
    },
    async fileXzipEncode(pathname: string): Promise<string> {
        if (!fs.existsSync(pathname)) {
            throw new Error(`Input file not found: ${pathname}`);
        }

        // Generate new filepath with .xzip extension
        const basePath = pathname.substring(0, pathname.lastIndexOf("."));
        const outputPath = basePath + ".xzip";

        // Get file info
        const fileStats = fs.statSync(pathname);
        const fileSize = fileStats.size;
        const fileExt = pathname.split(".").pop() || "";

        // Generate random 16-character key
        const encryptionKey = StrUtil.randomString(16);

        // Create metadata
        const filemeta = {
            version: 1,
            format: fileExt,
            size: fileSize,
            key: encryptionKey,
        };

        // Convert metadata to JSON and then base64 encode
        const metaJson = JSON.stringify(filemeta);
        const metaB64 = Buffer.from(metaJson, "utf-8").toString("base64");
        const metaLength = metaB64.length;

        // Prepare encryption key
        const keyBytes = Buffer.from(encryptionKey, "utf-8");
        const keyLength = keyBytes.length;

        // Stream processing: read, encrypt and write in chunks
        const inputStream = fs.createReadStream(pathname);
        const outputStream = fs.createWriteStream(outputPath);

        // Write metadata length (4 bytes, little-endian)
        const metaLengthBuffer = Buffer.allocUnsafe(4);
        metaLengthBuffer.writeUInt32LE(metaLength, 0);
        outputStream.write(metaLengthBuffer);

        // Write base64 encoded metadata
        outputStream.write(Buffer.from(metaB64, "utf-8"));

        // Stream encrypt the file content
        let bytesProcessed = 0;
        return new Promise((resolve, reject) => {
            inputStream.on("data", (chunk: Buffer) => {
                // XOR encrypt the chunk
                const encryptedChunk = Buffer.alloc(chunk.length);
                for (let i = 0; i < chunk.length; i++) {
                    encryptedChunk[i] = chunk[i] ^ keyBytes[bytesProcessed % keyLength];
                    bytesProcessed++;
                }

                // Write encrypted chunk
                outputStream.write(encryptedChunk);
            });

            inputStream.on("end", () => {
                outputStream.end();
                resolve(outputPath);
            });

            inputStream.on("error", error => {
                outputStream.destroy();
                reject(error);
            });

            outputStream.on("error", error => {
                inputStream.destroy();
                reject(error);
            });
        });
    },
    async fileXzipDecode(pathname: string): Promise<string> {
        if (!fs.existsSync(pathname)) {
            throw new Error(`Input file not found: ${pathname}`);
        }

        if (!pathname.endsWith(".xzip")) {
            return pathname; // Not an xzip file, return as is
        }

        let outputPath = pathname.replace(/\.xzip$/, "");

        return new Promise((resolve, reject) => {
            const inputStream = fs.createReadStream(pathname);
            let metadataRead = false;
            let filemeta: any = null;
            let keyBytes: Buffer;
            let bytesProcessed = 0;
            let outputStream: fs.WriteStream;
            let remainingMetaBytes = 0;
            let metaBuffer = Buffer.alloc(0);

            inputStream.on("data", (chunk: Buffer) => {
                let chunkOffset = 0;

                if (!metadataRead) {
                    if (remainingMetaBytes === 0) {
                        // Read metadata length (first 4 bytes)
                        if (chunk.length < 4) {
                            reject(new Error("Invalid xzip file: insufficient data for metadata length"));
                            return;
                        }
                        const metaLength = chunk.readUInt32LE(0);
                        remainingMetaBytes = metaLength;
                        chunkOffset = 4;
                    }

                    // Read metadata
                    const availableMetaBytes = Math.min(remainingMetaBytes, chunk.length - chunkOffset);
                    const metaChunk = chunk.subarray(chunkOffset, chunkOffset + availableMetaBytes);
                    metaBuffer = Buffer.concat([metaBuffer, metaChunk] as readonly Uint8Array[]);
                    remainingMetaBytes -= availableMetaBytes;
                    chunkOffset += availableMetaBytes;

                    if (remainingMetaBytes === 0) {
                        // Parse metadata
                        try {
                            const metaB64 = metaBuffer.toString("utf-8");
                            const metaJson = Buffer.from(metaB64, "base64").toString("utf-8");
                            filemeta = JSON.parse(metaJson);
                            keyBytes = Buffer.from(filemeta.key, "utf-8");

                            // Create output file with correct extension
                            const finalOutputPath = outputPath + (filemeta.format ? "." + filemeta.format : "");
                            outputStream = fs.createWriteStream(finalOutputPath);

                            metadataRead = true;

                            // Set the final output path for resolution
                            outputPath = finalOutputPath;
                        } catch (error) {
                            reject(new Error("Invalid xzip file: corrupted metadata"));
                            return;
                        }
                    }
                }

                if (metadataRead && chunkOffset < chunk.length) {
                    // Decrypt remaining chunk data
                    const encryptedChunk = chunk.subarray(chunkOffset);
                    const decryptedChunk = Buffer.alloc(encryptedChunk.length);
                    const keyLength = keyBytes.length;

                    for (let i = 0; i < encryptedChunk.length; i++) {
                        decryptedChunk[i] = encryptedChunk[i] ^ keyBytes[bytesProcessed % keyLength];
                        bytesProcessed++;
                    }

                    outputStream.write(decryptedChunk);
                }
            });

            inputStream.on("end", () => {
                if (outputStream) {
                    outputStream.end();
                    resolve(outputPath);
                } else {
                    reject(new Error("Invalid xzip file: incomplete metadata"));
                }
            });

            inputStream.on("error", error => {
                if (outputStream) {
                    outputStream.destroy();
                }
                reject(error);
            });

            if (outputStream) {
                outputStream.on("error", error => {
                    inputStream.destroy();
                    reject(error);
                });
            }
        });
    },
};

export const IconvUtil = {
    convert(str: string, to?: string, from?: string) {
        if (!from) {
            from = chardet.detect(Buffer.from(str));
        }
        to = to || "utf8";
        const buffer = iconvLite.encode(str, from);
        return iconvLite.decode(buffer, to);
    },
    bufferToUtf8(buffer: Buffer) {
        const encoding = chardet.detect(buffer);
        // if ('ISO-2022-CN' === encoding) {
        //     const iconvInstance = new Iconv('ISO-2022-CN', 'UTF-8//TRANSLIT//IGNORE');
        //     return iconvInstance.convert(buffer).toString()
        // }
        return iconvLite.decode(buffer, encoding).toString();
    },
    detect(buffer: Uint8Array) {
        // detect str encoding
        return chardet.detect(buffer);
    },
};

export const StrUtil = {
    randomString(len: number = 32) {
        const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        let result = "";
        for (let i = len; i > 0; --i) {
            result += chars[Math.floor(Math.random() * chars.length)];
        }
        return result;
    },
    uuid() {
        return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
            const r = (Math.random() * 16) | 0;
            const v = c === "x" ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        });
    },
    hashCode(str: string, length: number = 8) {
        let hash = 0;
        if (str.length === 0) return hash + "";
        for (let i = 0; i < str.length; i++) {
            const char = str.charCodeAt(i);
            hash = (hash << 5) - hash + char;
            hash = hash & hash;
        }
        let result = Math.abs(hash).toString(16);
        if (result.length < length) {
            result = "0".repeat(length - result.length) + result;
        }
        return result;
    },
    hashCodeWithDuplicateCheck(str: string, check: string[], length: number = 8) {
        let code = this.hashCode(str, length);
        while (check.includes(code)) {
            code = this.uuid().substring(0, length);
        }
        return code;
    },
    bigIntegerId() {
        return [Date.now(), (Math.floor(Math.random() * 1000000) + "").padStart(6, "0")].join("");
    },
    ucFirst(str: string) {
        if (!str) return "";
        return str.charAt(0).toUpperCase() + str.slice(1);
    },
};

export const TimeUtil = {
    timestampInMs() {
        return Date.now();
    },
    timestamp() {
        return Math.floor(Date.now() / 1000);
    },
    format(time: number, format: string = "YYYY-MM-DD HH:mm:ss") {
        return dayjs(time).format(format);
    },
    formatDate(time: number) {
        return dayjs(time).format("YYYY-MM-DD");
    },
    dateString() {
        return dayjs().format("YYYYMMDD");
    },
    datetimeString() {
        return dayjs().format("YYYYMMDD_HHmmss_SSS");
    },
    timestampDayStart(msTimestamp?: number) {
        let date = msTimestamp ? new Date(msTimestamp) : new Date();
        date.setHours(0, 0, 0, 0);
        return Math.floor(date.getTime() / 1000);
    },
    replacePattern(text: string) {
        // @ts-ignore
        return text.replaceAll("{year}", dayjs().format("YYYY"))
            .replaceAll("{month}", dayjs().format("MM"))
            .replaceAll("{day}", dayjs().format("DD"))
            .replaceAll("{hour}", dayjs().format("HH"))
            .replaceAll("{minute}", dayjs().format("mm"))
            .replaceAll("{second}", dayjs().format("ss"));
    },
};

export const FileUtil = {
    MIME_TYPES: {
        html: "text/html",
        htm: "text/html",
        js: "application/javascript",
        css: "text/css",
        json: "application/json",
        png: "image/png",
        jpg: "image/jpeg",
        jpeg: "image/jpeg",
        gif: "image/gif",
        svg: "image/svg+xml",
        webp: "image/webp",
        woff: "font/woff",
        woff2: "font/woff2",
        ttf: "font/ttf",
        otf: "font/otf",
        mp3: "audio/mpeg",
        mp4: "video/mp4",
        wav: "audio/wav",
        wasm: "application/wasm",
        eot: "application/vnd.ms-fontobject",
    },
    getMimeByExt(ext: string, defaultMime: string = ""): string {
        ext = ext.toLowerCase();
        if (ext.startsWith(".")) {
            ext = ext.substring(1);
        }
        return FileUtil.MIME_TYPES[ext] || defaultMime;
    },
    getMimeByPath(p: string, defaultMime: string = ""): string {
        const extension = p.split(".").pop().toLowerCase();
        return FileUtil.getMimeByExt(extension, defaultMime);
    },
    streamToBase64(stream: NodeJS.ReadableStream): Promise<string> {
        return new Promise((resolve, reject) => {
            const chunks = [];
            stream.on("data", chunk => {
                chunks.push(chunk);
            });
            stream.on("end", () => {
                const buffer = Buffer.concat(chunks);
                resolve(buffer.toString("base64"));
            });
            stream.on("error", error => {
                reject(error);
            });
        });
    },
    bufferToBase64(buffer: Buffer) {
        let binary = "";
        let bytes = new Uint8Array(buffer);
        let len = bytes.byteLength;
        for (let i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return EncodeUtil.base64Encode(binary);
    },
    base64ToBuffer(base64: string): Buffer {
        if (base64.startsWith("data:")) {
            base64 = base64.split("base64,")[1];
        }
        return Buffer.from(base64, "base64");
    },
    formatSize(size: number) {
        if (size < 1024) {
            return size + "B";
        } else if (size < 1024 * 1024) {
            return (size / 1024).toFixed(2) + "KB";
        } else if (size < 1024 * 1024 * 1024) {
            return (size / 1024 / 1024).toFixed(2) + "MB";
        } else {
            return (size / 1024 / 1024 / 1024).toFixed(2) + "GB";
        }
    },
    async md5(filePath: string) {
        return new Promise((resolve, reject) => {
            const hash = crypto.createHash("md5");
            const stream = fs.createReadStream(filePath);
            stream.on("data", data => {
                hash.update(data);
            });
            stream.on("end", () => {
                resolve(hash.digest("hex"));
            });
            stream.on("error", error => {
                reject(error);
            });
        });
    },
};

export const JsonUtil = {
    stringifyOrdered(obj: any) {
        return JSON.stringify(obj, Object.keys(obj).sort(), 4);
    },
    stringifyValueOrdered(obj: any) {
        const sortedData = Object.fromEntries(
            Object.entries(obj).sort(([, a], [, b]) => {
                // @ts-ignore
                return ((a as any) - b) as any;
            })
        );
        return JSON.stringify(sortedData, null, 4);
    },
};

export const ImportUtil = {
    async loadCommonJs(cjsPath: string, forceReload: boolean = true) {
        let tempPath = cjsPath;
        if (forceReload) {
            const md5 = await FileUtil.md5(cjsPath);
            tempPath = path.join(
                await FileIndex.tempDir('commonJs'),
                `${md5}.cjs`,
            )
            if (!fs.existsSync(tempPath)) {
                fs.copyFileSync(cjsPath, tempPath);
            }
        }
        const backend = await import(/* @vite-ignore */ `file://${tempPath}`);
        // console.log('loadCommonJs', `${cjsPath}?t=${md5}`)
        return backend.default;
    },
};

export const MemoryCacheUtil = {
    pool: {} as {
        [key: string]: {
            value: any;
            expire: number;
        };
    },
    _gc() {
        const now = TimeUtil.timestamp();
        for (const key in this.pool) {
            if (this.pool[key].expire < now) {
                delete this.pool[key];
            }
        }
    },
    async remember(key: string, callback: () => Promise<any>, ttl: number = 60) {
        if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {
            return this.pool[key].value;
        }
        const value = await callback();
        this.pool[key] = {
            value,
            expire: TimeUtil.timestamp() + ttl,
        };
        this._gc();
        return value;
    },
    get(key: string) {
        if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {
            return this.pool[key].value;
        }
        this._gc();
        return null;
    },
    set(key: string, value: any, ttl: number = 86400) {
        this.pool[key] = {
            value,
            expire: TimeUtil.timestamp() + ttl,
        };
        this._gc();
    },
    forget(key: string) {
        delete this.pool[key];
    },
};

export const MemoryMapCacheUtil = {
    pool: {} as {
        [group: string]: {
            [key: string]: {
                value: any;
                expire: number;
            };
        };
    },
    _gc() {
        const now = TimeUtil.timestamp();
        for (const group in this.pool) {
            for (const key in this.pool[group]) {
                if (this.pool[group][key].expire < now) {
                    delete this.pool[group][key];
                }
            }
        }
    },
    get(group: string, key: string) {
        if (this.pool[group] && this.pool[group][key] && this.pool[group][key].expire > TimeUtil.timestamp()) {
            return this.pool[group][key].value;
        }
        this._gc();
        return null;
    },
    set(group: string, key: string, value: any, ttl: number = 86400) {
        if (!this.pool[group]) {
            this.pool[group] = {};
        }
        this.pool[group][key] = {
            value,
            expire: TimeUtil.timestamp() + ttl,
        };
        this._gc();
    },
    forget(group: string, key: string) {
        if (this.pool[group]) {
            delete this.pool[group][key];
        }
    },
};

export const ShellUtil = {
    quotaPath(p: string) {
        return `"${p}"`;
    },
    parseCommandArgs(command: string) {
        let args = [];
        let arg = "";
        let quote = "";
        let escape = false;
        for (let i = 0; i < command.length; i++) {
            const c = command[i];
            if (escape) {
                arg += c;
                escape = false;
                continue;
            }
            if ("\\" === c) {
                escape = true;
                arg += c;
                continue;
            }
            if ("" === quote && (" " === c || "\t" === c)) {
                if (arg) {
                    args.push(arg);
                    arg = "";
                }
                continue;
            }
            if ("" === quote && ('"' === c || "'" === c)) {
                quote = c;
                arg += c;
                continue;
            }
            if ('"' === quote && '"' === c) {
                quote = "";
                arg += c;
                continue;
            }
            if ("'" === quote && "'" === c) {
                quote = "";
                arg += c;
                continue;
            }
            arg += c;
        }
        if (arg) {
            args.push(arg);
        }
        return args;
    },
};

export const VersionUtil = {
    /**
     * 检测版本是否匹配
     * @param v string
     * @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0
     */
    match(v: string, match: string) {
        if (match === "*") {
            return true;
        }
        if (match.startsWith(">=")) {
            if (this.ge(v, match.substring(2))) {
                return true;
            }
        } else if (match.startsWith("<=")) {
            if (this.le(v, match.substring(2))) {
                return true;
            }
        } else if (match.startsWith(">")) {
            if (this.gt(v, match.substring(1))) {
                return true;
            }
        } else if (match.startsWith("<")) {
            if (this.lt(v, match.substring(1))) {
                return true;
            }
        } else {
            return this.eq(v, match);
        }
        return false;
    },
    compare(v1: string, v2: string) {
        const v1Arr = v1.split(".");
        const v2Arr = v2.split(".");
        for (let i = 0; i < v1Arr.length; i++) {
            const v1Num = parseInt(v1Arr[i]);
            const v2Num = parseInt(v2Arr[i]);
            if (v1Num > v2Num) {
                return 1;
            } else if (v1Num < v2Num) {
                return -1;
            }
        }
        return 0;
    },
    gt(v1: string, v2: string) {
        return VersionUtil.compare(v1, v2) > 0;
    },
    ge(v1: string, v2: string) {
        return VersionUtil.compare(v1, v2) >= 0;
    },
    lt(v1: string, v2: string) {
        return VersionUtil.compare(v1, v2) < 0;
    },
    le: (v1: string, v2: string) => {
        return VersionUtil.compare(v1, v2) <= 0;
    },
    eq: (v1: string, v2: string) => {
        return VersionUtil.compare(v1, v2) === 0;
    },
};

export const UIUtil = {
    sizeToPx(size: string, sizeFull: number) {
        if (/^\d+$/.test(size)) {
            // 纯数字
            return parseInt(size);
        } else if (size.endsWith("%")) {
            // 百分比
            let result = Math.floor((sizeFull * parseInt(size)) / 100);
            result = Math.min(result, sizeFull);
            return result;
        } else {
            throw "UnsupportSizeString";
        }
    },
};

export const ReUtil = {
    match(regex: string, text: string) {
        if ("" === regex || null === regex) {
            return false;
        }
        if (regex.startsWith("/")) {
            const index = regex.lastIndexOf("/");
            const source = regex.slice(1, index);
            const flags = regex.slice(index + 1);
            return new RegExp(source, flags).test(text);
        }
        return new RegExp(regex).test(text);
    },
};

const converter = new Showdown.Converter({
    tables: true,
});
export const MarkdownUtil = {
    toHtml(markdown: string): string {
        return converter.makeHtml(markdown);
    },
};

type HotkeyModifierType = "Control" | "Option" | "Command" | "Ctrl" | "Alt" | "Win" | "Meta" | "Shift";
type HotkeyType = { key: string; modifiers: HotkeyModifierType[] };

export const HotKeyUtil = {
    orderModifiers(modifiers: HotkeyModifierType[]) {
        const order = ["Control", "Ctrl", "Command", "Meta", "Win", "Option", "Alt", "Shift"];
        return modifiers.sort((a, b) => {
            return order.indexOf(a) - order.indexOf(b);
        });
    },
    unifyObject(hotkey: HotkeyType) {
        return {
            key: hotkey.key.toUpperCase(),
            modifiers: this.orderModifiers(hotkey.modifiers.map(modifier => StrUtil.ucFirst(modifier))),
        };
    },
    unifyString(hotkey: string): HotkeyType {
        const parts = hotkey.split("+");
        const key = parts.pop() || "";
        const modifiers: any[] = [];
        parts.forEach(part => {
            modifiers.push(StrUtil.ucFirst(part.trim()));
        });
        return this.unifyObject({key, modifiers});
    },
    unify(hotkeys: string | string[] | HotkeyType | HotkeyType[]): HotkeyType[] {
        if (typeof hotkeys === "string") {
            return [this.unifyString(hotkeys)];
        } else if (Array.isArray(hotkeys)) {
            return hotkeys.map(hotkey => {
                if (typeof hotkey === "string") {
                    return this.unifyString(hotkey);
                } else {
                    return this.unifyObject(hotkey);
                }
            });
        } else {
            return [this.unifyObject(hotkeys)];
        }
    },
    getFromEvent(event: any): HotkeyType | null {
        const valid = [
            "A",
            "B",
            "C",
            "D",
            "E",
            "F",
            "G",
            "H",
            "I",
            "J",
            "K",
            "L",
            "M",
            "N",
            "O",
            "P",
            "Q",
            "R",
            "S",
            "T",
            "U",
            "V",
            "W",
            "X",
            "Y",
            "Z",
            "1",
            "2",
            "3",
            "4",
            "5",
            "6",
            "7",
            "8",
            "9",
            "0",
            "Space",
        ];
        const key = (event.key || "").toUpperCase();
        if (!event || !event.key || !valid.includes(key)) {
            return null;
        }
        const modifiers: HotkeyModifierType[] = [];
        if (isWin) {
            if (event.ctrlKey || event.control) {
                modifiers.push("Ctrl");
            }
            if (event.altKey || event.alt) {
                modifiers.push("Alt");
            }
            if (event.metaKey || event.meta) {
                modifiers.push("Win");
            }
        } else if (isMac) {
            if (event.ctrlKey || event.control) {
                modifiers.push("Control");
            }
            if (event.altKey || event.alt) {
                modifiers.push("Option");
            }
            if (event.metaKey || event.meta) {
                modifiers.push("Command");
            }
        } else {
            if (event.ctrlKey || event.control) {
                modifiers.push("Ctrl");
            }
            if (event.altKey || event.alt) {
                modifiers.push("Alt");
            }
            if (event.metaKey || event.meta) {
                modifiers.push("Meta");
            }
        }
        if (event.shiftKey || event.shift) {
            modifiers.push("Shift");
        }
        return this.unifyObject({key, modifiers});
    },
    match(hotkeysForMatch: HotkeyType[], hotkey: HotkeyType): boolean {
        if (!hotkeysForMatch || !hotkey) {
            return false;
        }
        const hotKeyStr = hotkey.modifiers.join("+") + "+" + hotkey.key;
        for (const key of hotkeysForMatch) {
            const keyStr = key.modifiers.join("+") + "+" + key.key;
            if (keyStr === hotKeyStr) {
                return true;
            }
        }
        return false;
    },
};
