/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository

'Shell commands' plugin for Obsidian.
Copyright (C) 2021 - 2024 Jarkko Linnanvirta

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
*/

'use strict';

var obsidian = require('obsidian');
var os = require('os');
var path = require('path');
var electron = require('electron');
var fs = require('fs');
var process$1 = require('process');
var view = require('@codemirror/view');
var child_process = require('child_process');

function _interopNamespace(e) {
    if (e && e.__esModule) return e;
    var n = Object.create(null);
    if (e) {
        Object.keys(e).forEach(function (k) {
            if (k !== 'default') {
                var d = Object.getOwnPropertyDescriptor(e, k);
                Object.defineProperty(n, k, d.get ? d : {
                    enumerable: true,
                    get: function () { return e[k]; }
                });
            }
        });
    }
    n["default"] = e;
    return Object.freeze(n);
}

var path__namespace = /*#__PURE__*/_interopNamespace(path);
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
var process__namespace = /*#__PURE__*/_interopNamespace(process$1);

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Cacheable {
    constructor() {
        this._caches = new Map;
        // Listen to SC_Plugin configuration changes.
        document.addEventListener("SC-configuration-change", () => {
            // Flush cache in order to get updated usages when needed.
            this._caches = new Map;
        });
    }
    cache(cacheKey, protagonist) {
        if (!this._caches.has(cacheKey)) {
            // No value is generated yet (or old value has been deleted before).
            this._caches.set(cacheKey, protagonist());
        }
        return this._caches.get(cacheKey);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Modal extends obsidian.Modal {
    constructor(plugin) {
        super(plugin.app);
        this.plugin = plugin;
        this._isOpen = false;
    }
    onOpen() {
        this._isOpen = true;
        // Make the modal scrollable if it has more content than what fits in the screen.
        this.modalEl.addClass("SC-modal", "SC-scrollable");
        // Approve the modal by pressing the enter key (if enabled).
        if (this.plugin.settings.approve_modals_by_pressing_enter_key) {
            this.scope.register([], "enter", (event) => {
                // Check that no textarea is focused and no autocomplete menu is open.
                if (0 === document.querySelectorAll("textarea:focus").length &&
                    0 === document.querySelectorAll("div.SC-autocomplete").length) {
                    // No textareas with focus and no open autocomplete menus were found.
                    this.approve();
                    event.preventDefault();
                    event.stopPropagation();
                }
            });
        }
    }
    isOpen() {
        return this._isOpen;
    }
    setTitle(title) {
        this.titleEl.innerText = title;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class ConfirmationModal extends SC_Modal {
    constructor(plugin, title, question, yes_button_text) {
        super(plugin);
        this.question = question;
        this.yes_button_text = yes_button_text;
        this.approved = false;
        /**
         * Can be used to add extra information to the modal, that will be shown between the modal's question and yes button.
         *
         * Note that when using this property, you SHOULD NOT overwrite existing content! Use extraContent.createEl() or
         * similar method that ADDS new content without replacing old content.
         */
        this.extraContent = document.createElement("div");
        this.setTitle(title);
        this.promise = new Promise((resolve) => {
            this.resolve_promise = resolve;
        });
    }
    onOpen() {
        super.onOpen();
        // Display the question
        this.modalEl.createEl("p", { text: this.question });
        // Display extra content/information. The element might be empty, if no extra content is added.
        this.modalEl.appendChild(this.extraContent);
        // Display the yes button
        new obsidian.Setting(this.modalEl)
            .addButton(button => button
            .setButtonText(this.yes_button_text)
            .onClick(() => this.approve()));
    }
    approve() {
        // Got a confirmation from a user
        this.resolve_promise(true);
        this.approved = true;
        this.close();
    }
    onClose() {
        super.onClose();
        if (!this.approved) { // TODO: Find out if there is a way to not use this kind of flag property. Can the status be checked from the promise itself?
            this.resolve_promise(false);
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * If true, logging stuff to console.log() will be enabled.
 * Might also enable some testing {{variables}} in the future, perhaps.
 */
let DEBUG_ON = false;
function setDEBUG_ON(value) {
    DEBUG_ON = value;
}
/**
 * Calls console.log(), but only if debugging is enabled.
 * @param messages
 */
function debugLog(...messages) {
    if (DEBUG_ON) {
        console.log(...messages);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class IDGenerator {
    constructor(reserved_ids = [], min_length = 10, characters = "abcdefghijklmnopqrstuvwxyz0123456789") {
        this.reserved_ids = reserved_ids;
        this.min_length = min_length;
        this.characters = characters;
    }
    addReservedID(id) {
        debugLog(IDGenerator.name + ": Adding id " + id + " to the list of reserved ids.");
        this.reserved_ids.push(id);
    }
    generateID() {
        let generated_id = "";
        while (generated_id.length < this.min_length || this.isIDReserved(generated_id)) {
            generated_id += this.generateCharacter();
        }
        this.reserved_ids.push(generated_id);
        debugLog(IDGenerator.name + ": Generated id " + generated_id);
        return generated_id;
    }
    getReservedIDs() {
        return this.reserved_ids;
    }
    generateCharacter() {
        return this.characters.charAt(Math.floor(Math.random() * this.characters.length));
    }
    isIDReserved(id) {
        return this.reserved_ids.contains(id);
    }
}
const id_generator = new IDGenerator();
function getIDGenerator() {
    return id_generator;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function getDefaultSettings(is_new_installation) {
    return {
        // Common:
        settings_version: is_new_installation
            ? SC_Plugin.SettingsVersion // For new installations, a specific settings version number can be used, as migrations do not need to be taken into account.
            : "prior-to-0.7.0" // This will be substituted by ShellCommandsPlugin.saveSettings() when the settings are saved.
        ,
        // Hidden settings (no UI controls in the settings panel)
        debug: false,
        obsidian_command_palette_prefix: "Execute: ",
        // Variables:
        preview_variables_in_command_palette: true,
        show_autocomplete_menu: true,
        // Environments:
        working_directory: "",
        default_shells: {},
        environment_variable_path_augmentations: {},
        show_installation_warnings: true,
        // Output:
        error_message_duration: 20,
        notification_message_duration: 10,
        execution_notification_mode: "disabled",
        output_channel_clipboard_also_outputs_to_notification: true,
        output_channel_notification_decorates_output: true,
        // Events:
        enable_events: true,
        // Modals:
        approve_modals_by_pressing_enter_key: true,
        // Obsidian's command palette:
        command_palette: {
            re_execute_last_shell_command: {
                enabled: true,
                prefix: "Re-execute: ",
            },
        },
        // Shell commands:
        max_visible_lines_in_shell_command_fields: false,
        shell_commands: [],
        // Prompts:
        prompts: [],
        // Additional configuration for built-in variables:
        builtin_variables: {},
        // Custom variables
        custom_variables: [],
        custom_variables_notify_changes_via: {
            obsidian_uri: true,
            output_assignment: true,
        },
        // Custom shells
        custom_shells: [],
        // Output wrappers
        output_wrappers: [],
    };
}
const PlatformNames = {
    darwin: "macOS",
    linux: "Linux",
    win32: "Windows",
};
/**
 * Same content as PlatformNames, but in a better accessible Map format.
 * TODO: Replace PlatformNames with this map, and convert usages of the old PlatformNames.
 */
const PlatformNamesMap = new Map(Object.entries(PlatformNames));
const CommandPaletteOptions = {
    enabled: "Command palette & hotkeys",
    unlisted: "Hotkeys only",
    disabled: "Excluded",
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function getVaultAbsolutePath(app) {
    // Original code was copied 2021-08-22 from https://github.com/phibr0/obsidian-open-with/blob/84f0e25ba8e8355ff83b22f4050adde4cc6763ea/main.ts#L66-L67
    // But the code has been rewritten 2021-08-27 as per https://github.com/obsidianmd/obsidian-releases/pull/433#issuecomment-906087095
    const adapter = app.vault.adapter;
    if (adapter instanceof obsidian.FileSystemAdapter) {
        return adapter.getBasePath();
    }
    throw new Error("Could not retrieve vault path. No DataAdapter was found from app.vault.adapter.");
}
function getPluginAbsolutePath(plugin, convertSlashToBackslash) {
    return normalizePath2(path__namespace.join(getVaultAbsolutePath(plugin.app), plugin.app.vault.configDir, "plugins", plugin.getPluginId()), convertSlashToBackslash);
}
/**
 * Retrieves a specific part of a version number.
 *
 * @param {string} wholeVersion - The complete version number string.
 * @param {"major" | "minor" | "patch"} part - The part of the version number to retrieve ("major", "minor", or "patch").
 *
 * @returns {string | null} - The specified part of the version number, or null if the part is not found.
 *
 * @example getVersionPart("2.3.4", "major"); // returns "2"
 * @example getVersionPart("2.3.4", "minor"); // returns "3"
 * @example getVersionPart("2.3.4", "patch"); // returns "4"
 * @example getVersionPart("2.3.4", "invalid-part-name"); // returns null
 */
function getVersionPart(wholeVersion, part) {
    const versionPartsMatch = wholeVersion.match(/^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/); // No $ at the end on purpose: if the version string continues, just ignore the rest.
    if (undefined === versionPartsMatch?.groups?.[part]) {
        return null;
    }
    return versionPartsMatch.groups[part];
}
/**
 * For some reason there is no Platform.isWindows .
 */
function isWindows() {
    return process__namespace.platform === "win32";
}
/**
 * This is just a wrapper around platform() in order to cast the type to PlatformId.
 * TODO: Consider renaming this to getCurrentPlatformId().
 */
function getOperatingSystem() {
    // @ts-ignore In theory, platform() can return an OS name not included in OperatingSystemName. But as Obsidian
    // currently does not support anything else than Windows, Mac and Linux (except mobile platforms, but they are
    // ruled out by the manifest of this plugin), it should be safe to assume that the current OS is one of those
    // three.
    return os.platform();
}
function getCurrentPlatformName() {
    return getPlatformName(getOperatingSystem());
}
function getPlatformName(platformId) {
    const platformName = PlatformNames[platformId];
    if (undefined === platformName) {
        throw new Error("Cannot find a platform name for: " + platformId);
    }
    return platformName;
}
/**
 * Tries to determine how Obsidian was installed. Used for displaying a warning if the installation type is "Flatpak".
 *
 * The logic is copied on 2023-12-20 from https://stackoverflow.com/a/75284996/2754026 .
 *
 * @return "Flatpak" | "AppImage" | "Snap" or `null`, if Obsidian was not installed using any of those methods, i.e. the installation method is unidentified.
 */
function getObsidianInstallationType() {
    if (process__namespace.env["container"]) {
        return "Flatpak";
    }
    else if (process__namespace.env["APPIMAGE"]) {
        return "AppImage";
    }
    else if (process__namespace.env["SNAP"]) {
        return "Snap";
    }
    return null;
}
function getView(app) {
    const view = app.workspace.getActiveViewOfType(obsidian.MarkdownView);
    if (!view) {
        debugLog("getView(): Could not get a view. Will return null.");
        return null;
    }
    return view;
}
function getEditor(app) {
    const view = getView(app);
    if (null === view) {
        // Could not get a view.
        return null;
    }
    // Ensure that view.editor exists! It exists at least if this is a MarkDownView.
    if ("editor" in view) {
        // Good, it exists.
        // @ts-ignore We already know that view.editor exists.
        return view.editor;
    }
    // Did not find an editor.
    debugLog("getEditor(): 'view' does not have a property named 'editor'. Will return null.");
    return null;
}
function cloneObject(object) {
    return Object.assign({}, object);
}
/**
 * Merges two or more objects together. If they have same property names, former objects' properties get overwritten by later objects' properties.
 *
 * @param objects
 */
function combineObjects(...objects) {
    return Object.assign({}, ...objects);
}
/**
 * Compares two objects deeply for equality.
 *
 * Copied 2023-12-30 from https://dmitripavlutin.com/how-to-compare-objects-in-javascript/#4-deep-equality
 * Modifications:
 *  - Added types to the function parameters and return value.
 *  - Changed `const val1 = object1[key];` to `const val1 = (object1 as {[key: string]: unknown})[key];`, and the same for val2.
 *  - Added a possibility to compare other values than objects, too.
 *
 * @param {unknown} object1 - The first object to compare.
 * @param {unknown} object2 - The second object to compare.
 * @return {boolean} - Returns `true` if the objects are deeply equal, `false` otherwise.
 * @author Original author: Dmitri Pavlutin
 */
function deepEqual(object1, object2) {
    if (!isObject(object1) || !isObject(object2)) {
        // If any of the parameters are not objects, do a simple comparison.
        return object1 === object2;
    }
    const keys1 = Object.keys(object1);
    const keys2 = Object.keys(object2);
    if (keys1.length !== keys2.length) {
        return false;
    }
    for (const key of keys1) {
        const val1 = object1[key];
        const val2 = object2[key];
        const areObjects = isObject(val1) && isObject(val2);
        if (areObjects && !deepEqual(val1, val2) ||
            !areObjects && val1 !== val2) {
            return false;
        }
    }
    return true;
}
/**
 * Copied 2023-12-30 from https://dmitripavlutin.com/how-to-compare-objects-in-javascript/#4-deep-equality
 * Modifications:
 *  - Added types to the function parameter and return value.
 *
 * Can be exported later, if needed elsewhere.
 *
 * @param object
 * @author Original author: Dmitri Pavlutin
 */
function isObject(object) {
    return object != null && typeof object === 'object';
}
/**
 * Gets the surplus properties from an object that are not present in another object.
 * @param {object} surplusObject - The object to check for surplus properties.
 * @param {object} comparisonObject - The object to compare against.
 * @return {object} - An object containing the surplus properties found in surplusObject that are not present in comparisonObject.
 */
function getObjectSurplusProperties(surplusObject, comparisonObject) {
    const surplusProperties = {};
    for (const key of Object.getOwnPropertyNames(surplusObject)) {
        if (!comparisonObject.hasOwnProperty(key)) {
            surplusProperties[key] = surplusObject[key];
        }
    }
    return surplusProperties;
}
/**
 * Assigns properties from defaultObject to targetObject, if they don't exist yet in the target. Existing properties are
 * NOT overridden.
 *
 * This can be thought of as merging two objects together, but inline, as opposed to combineObjects(), which creates a
 * new object.
 *
 * @param targetObject
 * @param defaultObject
 */
function ensureObjectHasProperties(targetObject, defaultObject) {
    for (const defaultPropertyName of Object.getOwnPropertyNames(defaultObject)) {
        if (undefined === targetObject[defaultPropertyName]) {
            // A property does not exist on targetObject. Create it, and use a value from defaultObject.
            targetObject[defaultPropertyName] = defaultObject[defaultPropertyName];
        }
    }
}
function mergeSets(set1, set2) {
    return new Set([...set1, ...set2]);
}
/**
 * Returns a new Set cloned from 'from_set', with all items presented in 'remove' removed from it.
 *
 * @param from_set
 * @param remove Can be either a Set of removable items, or a single item.
 */
function removeFromSet(from_set, remove) {
    const reduced_set = new Set(from_set);
    if (remove instanceof Set) {
        for (const removable of remove) {
            reduced_set.delete(removable);
        }
    }
    else {
        reduced_set.delete(remove);
    }
    return reduced_set;
}
/**
 * Same as normalizePath(), but fixes these glitches:
 * - Leading forward slashes / backward slashes should not be removed.
 * - \ should not be converted to / if platform is Windows. In other words, / should be converted to \ if platform is Windows.
 *
 * TODO: I've opened a discussion about this on Obsidian's forums. If anything new comes up in the discussion, make changes accordingly. https://forum.obsidian.md/t/normalizepath-removes-a-leading/24713
 */
function normalizePath2(path, convertSlashToBackslash) {
    // 1. Preparations
    path = path.trim();
    const leading_slashes_regexp = /^[/\\]*/gu; // Get as many / or \ slashes as there are in the very beginning of path. Can also be "" (an empty string).
    const leading_slashes_array = leading_slashes_regexp.exec(path); // An array with only one item.
    if (null === leading_slashes_array) {
        // It should always match. This exception should never happen, but have it just in case.
        throw new Error("normalizePath2(): leading_slashes_regexp did not match.");
    }
    let leading_slashes = leading_slashes_array[0];
    // 2. Run the original normalizePath()
    path = obsidian.normalizePath(path);
    // 3. Fixes
    // Check that correct slashes are used.
    if (convertSlashToBackslash) {
        // Convert / to \ (usually done when running on Windows, but might in theory happen on other platforms, too, if using a shell that uses Windows directory separators).
        path = path.replace(/\//gu, "\\"); // Need to use a regexp instead of a normal "/" -> "\\" replace because the normal replace would only replace first occurrence of /.
        leading_slashes = leading_slashes.replace(/\//gu, "\\"); // Same here.
    }
    // Now ensure that path still contains leading slashes (if there were any before calling normalizePath()).
    // Check that the path should have a similar set of leading slashes at the beginning. It can be at least "/" (on linux/Mac), or "\\" (on Windows when it's a network path), in theory even "///" or "\\\\\" whatever.
    // normalizePath() seems to remove leading slashes (and they are needed to be re-added), but it's needed to check first, otherwise the path would have double leading slashes if normalizePath() gets fixed in the future.
    if (leading_slashes.length && path.slice(0, leading_slashes.length) !== leading_slashes) {
        // The path does not contain the required set of leading slashes, so add them.
        path = leading_slashes + path;
    }
    // 4. Done
    return path;
}
function extractFileName(file_path, with_extension = true) {
    if (with_extension) {
        return path__namespace.parse(file_path).base;
    }
    else {
        return path__namespace.parse(file_path).name;
    }
}
function extractFileParentPath(file_path) {
    return path__namespace.parse(file_path).dir;
}
/**
 * On Windows: Checks if the given filePath exists WHEN ADDING any extension from the PATHEXT environment variable to the
 * end of the file path. This can notice that e.g. "CMD" exists as a file name, when it's checked as "CMD.EXE".
 * On other platforms than Windows: Returns always false.
 * Note: This DOES NOT CHECK existence of the original filePath without any additions. The caller should check it themselves.
 */
function lookUpFileWithBinaryExtensionsOnWindows(filePath) {
    if (isWindows()) {
        // Windows: Binary path may be missing a file extension, but it's still a valid and working path, so check
        // the path with additional extensions, too.
        const pathExt = process__namespace.env.PATHEXT ?? "";
        for (const extension of pathExt.split(";")) {
            if (fs__namespace.existsSync(filePath + extension)) {
                return true;
            }
        }
    }
    return false;
}
function joinObjectProperties(object, glue) {
    let result = "";
    for (const property_name in object) {
        if (result.length) {
            result += glue;
        }
        // @ts-ignore
        result += object[property_name];
    }
    return result;
}
/**
 * Removes all duplicates from an array.
 *
 * Idea is copied 2021-10-06 from https://stackoverflow.com/a/33121880/2754026
 */
function uniqueArray(array) {
    return [...new Set(array)];
}
/**
 * Opens a web browser in the specified URL.
 * @param url
 */
function gotoURL(url) {
    electron.shell.openExternal(url); // This returns a promise, but it can be ignored as there's nothing to do after opening the browser.
}
/**
 * TODO: Move to TShellCommand.
 *
 * @param plugin
 * @param aliasOrShellCommandContent
 */
function generateObsidianCommandName(plugin, aliasOrShellCommandContent) {
    const prefix = plugin.settings.obsidian_command_palette_prefix;
    return prefix + aliasOrShellCommandContent;
}
function isInteger(value, allow_minus) {
    if (allow_minus) {
        return !!value.match(/^-?\d+$/u);
    }
    else {
        return !!value.match(/^\d+$/u);
    }
}
function isScalar(value, allowNullAndUndefined) {
    if (null === value || undefined === value) {
        return allowNullAndUndefined;
    }
    const type = typeof value;
    return type !== 'object' && type !== 'function';
}
/**
 * Converts a string input to a floating-point number with limited decimal places. Replaces a possible comma with a dot.
 *
 * @param {string} input - The input string to be converted.
 * @param {number} countDecimals - The number of decimal places to limit the converted number to.
 * @return {number} - The converted floating-point number with limited decimal places.
 */
function inputToFloat(input, countDecimals) {
    const inputCommaReplaced = input.replace(",", ".");
    const number = parseFloat(inputCommaReplaced);
    const limitedDecimals = number.toFixed(countDecimals);
    return parseFloat(limitedDecimals); // Use parseFloat() again to remove a possible .0 and to convert it back to a number.
}
/**
 * Translates 1-indexed caret line and column to a 0-indexed EditorPosition object. Also translates a possibly negative line
 * to a positive line from the end of the file, and a possibly negative column to a positive column from the end of the line.
 * @param editor
 * @param caret_line
 * @param caret_column
 */
function prepareEditorPosition(editor, caret_line, caret_column) {
    // Determine line
    if (caret_line < 0) {
        // Negative line means to calculate it from the end of the file.
        caret_line = Math.max(0, editor.lastLine() + caret_line + 1);
    }
    else {
        // Positive line needs just a small adjustment.
        // Editor line is zero-indexed, line numbers are 1-indexed.
        caret_line -= 1;
    }
    // Determine column
    if (caret_column < 0) {
        // Negative column means to calculate it from the end of the line.
        caret_column = Math.max(0, editor.getLine(caret_line).length + caret_column + 1);
    }
    else {
        // Positive column needs just a small adjustment.
        // Editor column is zero-indexed, column numbers are 1-indexed.
        caret_column -= 1;
    }
    return {
        line: caret_line,
        ch: caret_column,
    };
}
function getSelectionFromTextarea(textarea_element, return_null_if_empty) {
    const selected_text = textarea_element.value.substring(textarea_element.selectionStart, textarea_element.selectionEnd);
    return "" === selected_text && return_null_if_empty ? null : selected_text;
}
/**
 * Creates an HTMLElement (with freely decidable tag) and adds the given content into it as normal text. No HTML formatting
 * is supported, i.e. possible HTML special characters are shown as-is. Newline characters are converted to <br> elements.
 *
 * @param tag
 * @param content
 * @param parent_element
 */
function createMultilineTextElement(tag, content, parent_element) {
    const content_element = parent_element.createEl(tag);
    // Insert content line-by-line
    const content_lines = content.split(/\r\n|\r|\n/g); // Don't use ( ) with | because .split() would then include the newline characters in the resulting array.
    content_lines.forEach((content_line, content_line_index) => {
        // Insert the line.
        content_element.insertAdjacentText("beforeend", content_line);
        // Insert a linebreak <br> if needed.
        if (content_line_index < content_lines.length - 1) {
            content_element.insertAdjacentHTML("beforeend", "<br>");
        }
    });
    return content_element;
}
const CalloutIcons = {
    note: "lucide-pencil",
    abstract: "lucide-clipboard-list",
    info: "lucide-info",
    todo: "lucide-check-circle-2",
    tip: "lucide-flame",
    success: "lucide-check",
    question: "lucide-help-circle",
    warning: "lucide-alert-triangle",
    failure: "lucide-x",
    danger: "lucide-zap",
    bug: "lucide-bug",
    example: "lucide-list",
    quote: "lucide-quote",
};
/**
 * Creates a <div> structure that imitates Obsidian's callouts like they appear on notes.
 *
 * The HTML structure is looked up on 2023-12-20 from this guide's screnshots: https://forum.obsidian.md/t/obsidian-css-quick-guide/58178#an-aside-on-classes-5
 * @param containerElement
 * @param calloutType
 * @param title
 * @param content
 */
function createCallout(containerElement, calloutType, title, content) {
    // Root.
    const calloutRoot = containerElement.createDiv({ cls: "callout" });
    calloutRoot.dataset.callout = calloutType;
    // Title.
    const calloutTitle = calloutRoot.createDiv({ cls: "callout-title" });
    const calloutTitleIcon = calloutTitle.createDiv({ cls: "callout-icon" });
    obsidian.setIcon(calloutTitleIcon, CalloutIcons[calloutType]);
    const calloutTitleInner = calloutTitle.createDiv({ cls: "callout-title-inner" });
    if (title instanceof DocumentFragment) {
        calloutTitleInner.appendChild(title);
    }
    else {
        calloutTitleInner.appendText(title);
    }
    // Content.
    const calloutContent = calloutRoot.createDiv({ cls: "callout-content" });
    if (content instanceof DocumentFragment) {
        calloutContent.appendChild(content);
    }
    else {
        calloutContent.createEl("p").appendText(content);
    }
}
function randomInteger(min, max) {
    const range = max - min + 1;
    return min + Math.floor(Math.random() * range);
}
/**
 * Does the following prefixings:
 *   \ will become \\
 *   [ will become \[
 *   ] will become \]
 *   ( will become \(
 *   ) will become \)
 *
 * @param content
 */
function escapeMarkdownLinkCharacters(content) {
    // TODO: \[ can be replaced with [ as eslint suggests and ten remove the ignore line below. I'm not doing it now because it would be outside of the scope of this commit/issue #70.
    // eslint-disable-next-line no-useless-escape
    return content.replace(/[\\()\[\]]/gu, "\\$&");
}
function copyToClipboard(text) {
    return electron.clipboard.writeText(text);
}
function cloakPassword(password) {
    return "&bull;".repeat(password.length);
}
async function getFileContentWithoutYAML(app, file) {
    return new Promise((resolve) => {
        // The logic is borrowed 2022-09-01 from https://forum.obsidian.md/t/how-to-get-current-file-content-without-yaml-frontmatter/26197/2
        // Thank you, endorama! <3
        const file_content = app.vault.read(file);
        file_content.then((file_content) => {
            const frontmatterPosition = app.metadataCache.getFileCache(file)?.frontmatterPosition;
            if (frontmatterPosition) {
                // A YAML frontmatter is present in the file.
                const frontmatterEndLineNumber = frontmatterPosition.end.line + 1; // + 1: Take the last --- line into account, too.
                const file_content_without_frontmatter = file_content.split("\n").slice(frontmatterEndLineNumber).join("\n");
                return resolve(file_content_without_frontmatter);
            }
            else {
                // No YAML frontmatter is present in the file.
                // Return the whole file content, because there's nothing to remove.
                return resolve(file_content);
            }
        });
    });
}
async function getFileYAML(app, file, withDashes) {
    return new Promise((resolve) => {
        // The logic is borrowed 2022-09-01 from https://forum.obsidian.md/t/how-to-get-current-file-content-without-yaml-frontmatter/26197/2
        // Thank you, endorama! <3
        const fileContent = app.vault.read(file);
        fileContent.then((file_content) => {
            const frontmatterPosition = app.metadataCache.getFileCache(file)?.frontmatterPosition;
            if (frontmatterPosition) {
                // A YAML frontmatter is present in the file.
                const frontmatterEndLineNumber = frontmatterPosition.end.line + 1; // + 1: Take the last --- line into account, too.
                let firstLine;
                let lastLine;
                if (withDashes) {
                    // Take full YAML content, including --- lines at the top and bottom.
                    firstLine = 0;
                    lastLine = frontmatterEndLineNumber;
                }
                else {
                    // Exclude --- lines.
                    firstLine = 1;
                    lastLine = frontmatterEndLineNumber - 1;
                }
                const frontmatterContent = file_content.split("\n").slice(firstLine, lastLine).join("\n");
                return resolve(frontmatterContent);
            }
            else {
                // No YAML frontmatter is present in the file.
                return resolve(null);
            }
        });
    });
}
/**
 * Tries to provide a simple try...catch interface that rethrows unrecognised exceptions on your behalf.
 * @param act Try to do this. If it succeeds, tryTo() returns what act() returns, and does not touch the other arguments.
 * @param fix An exception handler that gets called if act() throws an exception that matches any one in bust. The function receives the caught exception as a parameter.
 * @param bust An array of Error classes that can be caught. Any other exceptions will be rethrown.
 */
function tryTo(act, fix, ...bust) {
    try {
        // Try to do stuff and see if an exception occurs.
        return act();
    }
    catch (exception) {
        // An exception has happened. Check if it's included in the list of handleable exceptions.
        const canCatch = bust.filter(catchable => exception instanceof catchable.constructor).length > 0;
        if (canCatch) {
            // This exception can be handled.
            return fix(exception);
        }
        else {
            // This exception cannot be handled. Rethrow it.
            throw exception;
        }
    }
}
/**
 * Values are 1-indexed.
 * @param viewUpdate
 */
function getCodeMirrorLineAndColumnNumbers(viewUpdate) {
    const oldPosition = viewUpdate.startState.selection.main.head;
    const newPosition = viewUpdate.state.selection.main.head;
    const oldLine = viewUpdate.startState.doc.lineAt(oldPosition);
    const newLine = viewUpdate.state.doc.lineAt(newPosition);
    return {
        old: {
            line: oldLine.number,
            column: oldPosition - oldLine.from + 1,
        },
        new: {
            line: newLine.number,
            column: newPosition - newLine.from + 1,
        },
    };
}

/**
 * Escapes a string that will be used as a pattern in a regular expression.
 *
 * Note that this does not escape minus: - . It's probably ok as long as you won't wrap the result of this function in square brackets [ ] . For more information, read a comment by coolaj86 on Nov 29, 2019 at 2:44 in this Stack Overflow answer: https://stackoverflow.com/a/6969486/2754026
 *
 * Copied 2022-03-10 from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
 * Modifications:
 *  - Added TypeScript data type hints for the parameter and return value.
 *  - Added 'export' keyword.
 *  - Added this JSDoc.
 *  - No other changes.
 *
 * @param string
 * @return string
 */
function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
const Documentation = {
    // Keep the nested structure in this order:
    // 1. 'folder' (if exists)
    // 2. 'index' (if exists)
    // 3. Page links in alphabetical order.
    // 4. Sub-folder objects in alphabetical order.
    index: "https://publish.obsidian.md/shellcommands",
    environments: {
        additionsToPathEnvironmentVariable: "https://publish.obsidian.md/shellcommands/Environments/Additions+to+the+PATH+environment+variable",
        customShells: {
            index: "https://publish.obsidian.md/shellcommands/Environments/Custom+shells/Custom+shells",
            settings: "https://publish.obsidian.md/shellcommands/Environments/Custom+shells/Settings+for+custom+shells",
        },
    },
    events: {
        folder: "https://publish.obsidian.md/shellcommands/Events/",
        debouncing: "https://publish.obsidian.md/shellcommands/Events/Events+-+debouncing",
    },
    outputHandling: {
        outputHandlingMode: "https://publish.obsidian.md/shellcommands/Output+handling/Realtime+output+handling",
        outputWrappers: "https://publish.obsidian.md/shellcommands/Output+handling/Output+wrappers",
        ansiCode: "https://publish.obsidian.md/shellcommands/Output+handling/Styling+for+output+text+with+ANSI+code",
    },
    problems: {
        flatpakInstallation: "https://publish.obsidian.md/shellcommands/Problems/Flatpak+installation",
    },
    variables: {
        folder: "https://publish.obsidian.md/shellcommands/Variables/",
        allVariables: "https://publish.obsidian.md/shellcommands/Variables/All+variables",
        autocomplete: {
            index: "https://publish.obsidian.md/shellcommands/Variables/Autocomplete/Autocomplete",
        },
        customVariables: "https://publish.obsidian.md/shellcommands/Variables/Custom+variables",
        passVariablesToStdin: "https://publish.obsidian.md/shellcommands/Variables/Pass+variables+to+stdin",
    },
};
const GitHub = {
    repository: "https://github.com/Taitava/obsidian-shellcommands",
    changelog: "https://github.com/Taitava/obsidian-shellcommands/blob/main/CHANGELOG.md",
    license: "https://github.com/Taitava/obsidian-shellcommands/blob/main/LICENSE",
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * Variables that can be used to inject values to shell commands using {{variable:argument}} syntax.
 */
class Variable {
    constructor(plugin) {
        this.plugin = plugin;
        /**
         * If this is false, the variable can be assigned a default value that can be used in situations where the variable is unavailable.
         * TODO: Set to false, as most Variables are not always available. Then remove all 'always_available = false' lines from subclasses, and add 'always_available = true' to those subclasses that need it.
         * @protected
         */
        this.always_available = true;
        this.app = plugin.app;
    }
    getValue(shell, t_shell_command = null, sc_event = null, variableArguments = {}, 
    /**
     * Will parse variables in a default value (only used if this variable is not available this time). The callback
     * is only used, if t_shell_command is given. Set to null, if no variable parsing is needed for default values.
     * */
    default_value_parser = null) {
        return new Promise((resolve) => {
            // Cast arguments (if any) to their correct data types
            const castedArguments = this.castArguments(variableArguments);
            // Generate a value, or catch an exception if one occurs.
            this.generateValue(shell, castedArguments, sc_event).then((value) => {
                // Value generation succeeded.
                return resolve({
                    value: value,
                    error_messages: [],
                    succeeded: true,
                });
            }).catch((error) => {
                // Caught a VariableError or an Error.
                if (error instanceof VariableError) {
                    // The variable is not available in this situation.
                    debugLog(this.constructor.name + ".getValue(): Caught a VariableError and will determine how to handle it: " + error.message);
                    // Check what should be done.
                    const default_value_configuration = this.getDefaultValueConfiguration(t_shell_command);
                    const default_value_type = default_value_configuration ? default_value_configuration.type : "show-errors";
                    const debug_message_base = "Variable " + this.getFullName() + " is not available. ";
                    switch (default_value_type) {
                        case "show-errors":
                            // Generate error messages by calling generateValue().
                            debugLog(debug_message_base + "Will prevent shell command execution and show visible error messages.");
                            return resolve({
                                value: null,
                                error_messages: [error.message],
                                succeeded: false,
                            });
                        case "cancel-silently":
                            // Prevent execution, but do not show any errors
                            debugLog(debug_message_base + "Will prevent shell command execution silently without visible error messages.");
                            return resolve({
                                value: null,
                                error_messages: [],
                                succeeded: false,
                            });
                        case "value":
                            // Return a default value.
                            if (!default_value_configuration) {
                                // This should not happen, because default_value_type is never "value" when default_value_configuration is undefined or null.
                                // This check is just for TypeScript compiler to understand that default_value_configuration is defined when it's accessed below.
                                throw new Error("Default value configuration is undefined.");
                            }
                            debugLog(debug_message_base + "Will use a default value: " + default_value_configuration.value);
                            if (default_value_parser) {
                                // Parse possible variables in the default value.
                                default_value_parser(default_value_configuration.value).then((default_value_parsing_result) => {
                                    return resolve({
                                        value: default_value_parsing_result.succeeded
                                            ? default_value_parsing_result.parsed_content
                                            : default_value_parsing_result.original_content,
                                        error_messages: default_value_parsing_result.error_messages,
                                        succeeded: default_value_parsing_result.succeeded,
                                    });
                                });
                            }
                            else {
                                // No variable parsing is wanted.
                                return resolve({
                                    value: default_value_configuration.value,
                                    error_messages: [],
                                    succeeded: true,
                                });
                            }
                            break;
                        default:
                            throw new Error("Unrecognised default value type: " + default_value_type);
                    }
                }
                else {
                    // A program logic error has happened.
                    debugLog(this.constructor.name + ".getValue(): Caught an unrecognised error of class: " + error.constructor.name + ". Will rethrow it.");
                    throw error;
                }
            });
        });
    }
    /**
     * Called from parseVariableSynchronously(), only used on some special Variables that are not included in
     * loadVariables()/SC_Plugin.getVariables().
     *
     * Can only support non-async Variables. Also, parameters are not supported, at least at the moment.
     */
    getValueSynchronously() {
        return tryTo(() => ({
            value: this.generateValueSynchronously(),
            succeeded: true,
            error_messages: [],
        }), (variableError) => ({
            value: null,
            succeeded: false,
            error_messages: [variableError.message],
        }), VariableError);
    }
    /**
     * Variables that support parseVariableSynchronously() should define this. Most Variables don't need this.
     */
    generateValueSynchronously() {
        throw new Error("generateValueSynchronously() is not implemented for " + this.constructor.name + ".");
        // Use Error instead of VariableError, because this is not a problem that a user could fix. It's a program error.
    }
    getParameters() {
        const child_class = this.constructor;
        return child_class.parameters;
    }
    getParameterSeparator() {
        const child_class = this.constructor;
        return child_class.parameter_separator;
    }
    getPattern() {
        const error_prefix = this.variable_name + ".getPattern(): ";
        let pattern = '\\{\\{!?' + escapeRegExp(this.variable_name);
        for (const parameter_name in this.getParameters()) {
            const parameter = this.getParameters()[parameter_name];
            let parameter_type_pattern = this.getParameterSeparator(); // Here this.parameter_separator (= : ) is included in the parameter value just so that it's not needed to do nested parenthesis to accomplish possible optionality: (:())?. parseShellCommandVariables() will remove the leading : .
            // Check should we use parameter.options or parameter.type.
            if (undefined === parameter.options &&
                undefined === parameter.type) {
                // Neither is defined :(
                throw Error(error_prefix + "Parameter '" + parameter_name + "' should define either 'type' or 'options', neither is defined!");
            }
            else if (undefined !== parameter.options &&
                undefined !== parameter.type) {
                // Both are defined :(
                throw Error(error_prefix + "Parameter '" + parameter_name + "' should define either 'type' or 'options', not both!");
            }
            else if (undefined !== parameter.options) {
                // Use parameter.options
                parameter_type_pattern += parameter.options.join("|" + this.getParameterSeparator()); // E.g. "absolute|:relative" for {{file_path:mode}} variable's 'mode' parameter.
            }
            else {
                // Use parameter.type
                switch (parameter.type) {
                    case "string":
                        parameter_type_pattern += ".*?";
                        break;
                    case "integer":
                        parameter_type_pattern += "\\d+";
                        break;
                    default:
                        throw Error(error_prefix + "Parameter '" + parameter_name + "' has an unrecognised type: " + parameter.type);
                }
            }
            // Add the subpattern to 'pattern'.
            pattern += "(" + parameter_type_pattern + ")";
            if (!parameter.required) {
                // Make the parameter optional.
                pattern += "?";
            }
        }
        pattern += '\\}\\}';
        return pattern;
    }
    getParameterNames() {
        return Object.getOwnPropertyNames(this.getParameters());
    }
    /**
     * @param variableArguments String typed arguments. Arguments that should be typed otherly, will be cast to other types. Then all arguments are returned.
     */
    castArguments(variableArguments) {
        const castedArguments = {};
        for (const parameterName of Object.getOwnPropertyNames(variableArguments)) {
            const parameter_type = this.getParameters()[parameterName].type ?? "string"; // If the variable uses "options" instead of "type", then the type is always "string".
            const argument = variableArguments[parameterName];
            switch (parameter_type) {
                case "string":
                    castedArguments[parameterName] = argument;
                    break;
                case "integer":
                    castedArguments[parameterName] = parseInt(argument);
                    break;
            }
        }
        return castedArguments;
    }
    /**
     * Creates a VariableError and passes it to a rejector function, which will pass the VariableError to Variable.getValue().
     * Then it will be handled there according to user preferences.
     *
     * @param message
     * @param rejector
     * @protected
     */
    reject(message, rejector) {
        rejector(this.newVariableError(message));
    }
    /**
     * Similar to Variable.reject(), but uses a traditional throw. Can be used in async methods. For methods that create
     * Promises manually, Variable.reject() should be used, because errors thrown in manually created Promises are not caught
     * by Variable.getValue()'s Promise.catch() callback.
     *
     * @param message
     * @protected
     */
    throw(message) {
        throw this.newVariableError(message);
    }
    newVariableError(message) {
        const prefix = this.getFullName() + ": ";
        return new VariableError(prefix + message);
    }
    getAutocompleteItems() {
        // Check if the variable has at least one _mandatory_ parameter.
        let parameter_indicator = "";
        const parameter_names = Object.getOwnPropertyNames(this.getParameters())
            .filter(parameter_name => this.getParameters()[parameter_name].required === true) // Only include mandatory parameters
        ;
        if (parameter_names.length > 0) {
            parameter_indicator = Variable.parameter_separator; // When the variable name ends with a parameter separator character, it indicates to a user that an argument should be supplied.
        }
        return [
            // Normal variable
            {
                value: "{{" + this.variable_name + parameter_indicator + "}}",
                help_text: (this.help_text + " " + this.getAvailabilityText()).trim(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped version of the variable
            {
                value: "{{!" + this.variable_name + parameter_indicator + "}}",
                help_text: (this.help_text + " " + this.getAvailabilityText()).trim(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>" + this.getFullName() + "</strong>";
    }
    /**
     * Returns the Variable's name wrapped in {{ and }}.
     *
     * TODO: Change hardcoded {{ }} entries to use this method all around the code.
     */
    getFullName(withExclamationMark = false, variableArguments) {
        if (typeof variableArguments === "string") {
            variableArguments = [variableArguments];
        }
        const variableArgumentsString = variableArguments?.length ? ":" + variableArguments.join(":") : ""; // Check .length too: empty array should not cause a colon to appear.
        const opening = withExclamationMark ? "{{!" : "{{";
        return opening + this.variable_name + variableArgumentsString + "}}";
    }
    /**
     * TODO: Create a class BuiltinVariable and move this method there. This should not be present for CustomVariables.
     * TODO: When creating the new class, remove `undefined` from the possible return types. Built-in variables are required to provide a documentation link.
     */
    getDocumentationLink() {
        if (this.constructor.name === "CustomVariable") { // Don't use `this instanceof CustomVariable`, because `import CustomVariable` would cause a circular dependency.
            // Variables created by users do not have documentation pages.
            return undefined;
        }
        return Documentation.variables.folder + encodeURI(this.getFullName());
    }
    /**
     * TODO: Create a class BuiltinVariable and move this method there. This should not be present for CustomVariables.
     */
    createDocumentationLinkElement(container) {
        if (this.constructor.name === "CustomVariable") { // Don't use `this instanceof CustomVariable`, because `import CustomVariable` would cause a circular dependency.
            throw new Error("Variable.createDocumentationLinkElement() was called upon a CustomVariable. It can only be called upon a built-in variable.");
        }
        const description = this.getFullName() + ": " + this.help_text
            + os.EOL + os.EOL +
            "Click for external documentation.";
        container.createEl("a", {
            text: this.getFullName(),
            href: this.getDocumentationLink(),
            attr: { "aria-label": description },
        });
    }
    /**
     * Returns a unique string that can be used in default value configurations.
     * @return Normal variable name, if this is a built-in variable; or an ID string if this is a CustomVariable.
     */
    getIdentifier() {
        return this.getFullName();
    }
    /**
     * This can be used to determine if the variable can sometimes be unavailable. Used in settings to allow a user to define
     * default values for variables that are not always available, filtering out always available variables for which default
     * values would not make sense.
     */
    isAlwaysAvailable() {
        return this.always_available;
    }
    /**
     * For variables that are always available, returns an empty string.
     */
    getAvailabilityText() {
        return "";
    }
    /**
     * Same as getAvailabilityText(), but removes HTML from the result.
     */
    getAvailabilityTextPlain() {
        return this.getAvailabilityText().replace(/<\/?strong>/ig, ""); // Remove <strong> and </strong> markings from the help text
    }
    /**
     * Returns a default value configuration object that should be used if a shell command does not define its own
     * default value configuration object.
     */
    getGlobalDefaultValueConfiguration() {
        // Works for built-in variables only. CustomVariable class needs to override this method and not call the parent!
        return this.plugin.settings.builtin_variables[this.getIdentifier()]?.default_value; // Can return null
    }
    /**
     * @param tShellCommand If defined, a default value configuration is first tried to be found from the TShellCommand. If null, or if the TShellCommand didn't contain a configuration (or if the configuration's type is "inherit"), returns a configuration from getGlobalDefaultValueConfiguration().
     * @return Returns an object complying to GlobalVariableDefaultValueConfiguration even if the configuration was found from a TShellCommand, because the returned configuration will never have type "inherit".
     */
    getDefaultValueConfiguration(tShellCommand) {
        const defaultValueConfigurationFromShellCommand = tShellCommand?.getDefaultValueConfigurationForVariable(this); // tShellCommand can be null, or the method can return null.
        if (!defaultValueConfigurationFromShellCommand || defaultValueConfigurationFromShellCommand.type === "inherit") {
            return this.getGlobalDefaultValueConfiguration(); // Also this method can return null.
        }
        return defaultValueConfigurationFromShellCommand; // For some reason TypeScript does not realize that defaultValueConfigurationFromShellCommand.type cannot be "inherit" in this situation, so the 'as ...' part is needed.
    }
    /**
     * Takes an array of IAutocompleteItems. Will add `{{!` (unescaped variable) versions for each {{variable}} it encounters.
     * The additions are done in-place, so the method returns nothing.
     *
     * @protected
     */
    static supplementAutocompleteItems(autocompleteItems) {
        const originalLength = autocompleteItems.length;
        for (let autocompleteItemIndex = 0; autocompleteItemIndex < originalLength; autocompleteItemIndex++) {
            const autocompleteItem = autocompleteItems[autocompleteItemIndex];
            if (autocompleteItem.value.match(/^\{\{[[^!].*}}$/)) {
                // This is a {{variable}} which does not have ! as the first character after {{.
                // Duplicate it.
                const duplicatedAutocompleteItem = Object.assign({}, autocompleteItem, {
                    value: autocompleteItem.value.replace(/^\{\{/, "{{!"),
                    type: "unescaped-variable",
                });
                autocompleteItems.push(duplicatedAutocompleteItem);
            }
        }
    }
}
Variable.parameter_separator = ":";
/**
 * A definition for what parameters this variables takes.
 * @protected
 */
Variable.parameters = {};
/**
 * Thrown when Variables encounter errors that users should solve. Variable.getValue() will catch these and show to user
 * (unless errors are ignored).
 */
class VariableError extends Error {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Output extends Variable {
    constructor(plugin, output_content) {
        super(plugin);
        this.output_content = output_content;
        this.variable_name = "output";
        this.help_text = "Gives text outputted by a shell command after it's executed.";
    }
    async generateValue() {
        return this.output_content;
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in <em>output wrappers</em>, cannot be used as input for shell commands.";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class EditorVariable extends Variable {
    constructor() {
        super(...arguments);
        this.always_available = false;
    }
    getEditorOrThrow() {
        const editor = getEditor(this.app);
        if (null === editor) {
            // No editor.
            this.throw("Could not get an editor instance! Please create a discussion in GitHub.");
        }
        return editor;
    }
    /**
     * Can be made protected if needed to be accessed by subclasses.
     * @private
     */
    getViewOrThrow() {
        const view = getView(this.app);
        if (null === view) {
            // No view.
            this.throw("Could not get a view instance! Please create a discussion in GitHub.");
        }
        return view;
    }
    requireViewModeSource() {
        const view = this.getViewOrThrow();
        const view_mode = view.getMode(); // "preview" or "source" ("live" was removed from Obsidian API in 0.13.8 on 2021-12-10).
        switch (view_mode) {
            case "preview":
                // The leaf is in preview mode, which makes things difficult.
                // FIXME: Make it possible to use this feature also in preview mode.
                debugLog("EditorVariable: 'view' is in preview mode, and the poor guy who wrote this code, does not know how to return an editor instance that could be used for getting text selection.");
                this.throw("You need to turn editing mode on, unfortunately this variable does not work in preview mode.");
                break;
            case "source":
                // Good, the editor is in "source" mode, so it's possible to get a selection, caret position or other editing related information.
                return;
            default:
                this.throw("Unrecognised view mode: " + view_mode);
                break;
        }
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> when a note pane is open, not in graph view, nor when viewing non-text files.";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_CaretParagraph extends EditorVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "caret_paragraph";
        this.help_text = "Gives a text line at the current caret position.";
    }
    async generateValue() {
        const editor = this.getEditorOrThrow();
        this.requireViewModeSource();
        const caretPosition = editor.getCursor('to');
        return editor.getLine(caretPosition.line);
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Not available in preview mode.";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact:
 *  - Vinay Rajur: https://github.com/vrajur
 *  - Jarkko Linnanvirta: https://github.com/Taitava/
 */
class Variable_CaretPosition extends EditorVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "caret_position";
        this.help_text = "Gives the line number and column position of the current caret position as 'line:column'. Get only the line number using {{caret_position:line}}, and only the column with {{caret_position:column}}. Line and column numbers are 1-indexed.";
    }
    async generateValue(shell, castedArguments) {
        // Check that we are able to get an editor
        const editor = this.getEditorOrThrow();
        const position = editor.getCursor('to');
        const line = position.line + 1; // editor position is zero-indexed, line numbers are 1-indexed
        const column = position.ch + 1; // editor position is zero-indexed, column positions are 1-indexed
        if (undefined !== castedArguments.mode) {
            switch (castedArguments.mode.toLowerCase()) {
                case "line":
                    return `${line}`;
                case "column":
                    return `${column}`;
                default:
                    this.throw("Unrecognised argument: " + castedArguments.mode);
            }
        }
        else {
            // default case when no args provided
            return `${line}:${column}`;
        }
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + "}}",
                help_text: "Gives the line number and column position of the current caret position as 'line:column'. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":line}}",
                help_text: "Gives the line number of the current caret position. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":column}}",
                help_text: "Gives the column number of the current caret position. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + "}}",
                help_text: "Gives the line number and column position of the current caret position as 'line:column'. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":line}}",
                help_text: "Gives the line number of the current caret position. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":column}}",
                help_text: "Gives the column number of the current caret position. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{caret_position}}</strong>, <strong>{{caret_position:line}}</strong> or <strong>{{caret_position:column}}</strong>";
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Not available in preview mode.";
    }
}
Variable_CaretPosition.parameters = {
    mode: {
        options: ["line", "column"],
        required: false,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Clipboard extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "clipboard";
        this.help_text = "Gives the content you last copied to your clipboard.";
    }
    async generateValue() {
        return electron.clipboard.readText();
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Date extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "date";
        this.help_text = "Gives a date/time stamp as per your liking. The \"format\" part can be customized and is mandatory. Formatting options: https://momentjs.com/docs/#/displaying/format/";
    }
    async generateValue(shell, castedArguments) {
        return obsidian.moment().format(castedArguments.format);
    }
}
Variable_Date.parameters = {
    format: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Environment extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "environment";
        this.help_text = "Gives an environment variable's value. It's an original value received when Obsidian was started.";
        this.always_available = false;
    }
    async generateValue(shell, castedArguments) {
        // Check that the requested environment variable exists.
        if (undefined !== process.env[castedArguments.variable]) {
            // Yes, it exists.
            return process.env[castedArguments.variable]; // as string: tells TypeScript compiler that the item exists, is not undefined.
        }
        else {
            // It does not exist.
            // Freak out.
            this.throw(`Environment variable named '${castedArguments.variable}' does not exist.`);
        }
    }
    getHelpName() {
        return "<strong>{{environment:variable}}</strong>";
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> if the passed environment variable name exists.";
    }
}
Variable_Environment.parameters = {
    variable: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class FileVariable extends Variable {
    constructor() {
        super(...arguments);
        this.always_available = false;
    }
    getFileOrThrow() {
        const currentFile = this.app.workspace.getActiveFile();
        if (!currentFile) {
            this.throw("No file is active at the moment. Open a file or click a pane that has a file open.");
        }
        return currentFile;
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> when the active pane contains a file, not in graph view or other non-file view.";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FileContent extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "file_content";
        this.help_text = "Gives the current file's content, including YAML frontmatter. If you need YAML excluded, use {{note_content}} instead.";
    }
    async generateValue() {
        // Retrieve file content.
        return await app.vault.read(this.getFileOrThrow());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * TODO: Consider creating a decorator class for TFolder and moving this function to be a method in it.
 *
 * @param app
 * @param shell
 * @param folder
 * @param mode
 */
function getFolderPath(app, shell, folder, mode) {
    switch (mode.toLowerCase()) {
        case "absolute":
            return shell.translateAbsolutePath(getVaultAbsolutePath(app) + "/" + folder.path);
        case "relative":
            if (folder.isRoot()) {
                // Obsidian API does not give a correct folder.path value for the vault's root folder.
                // TODO: See this discussion and apply possible changes if something will come up: https://forum.obsidian.md/t/vault-root-folders-relative-path-gives/24857
                return ".";
            }
            else {
                // This is a normal subfolder
                return shell.translateRelativePath(folder.path);
            }
    }
}
/**
 * TODO: Consider creating a decorator class for TFile and moving this function to be a method in it.
 *
 * @param app
 * @param shell
 * @param file
 * @param mode
 */
function getFilePath(app, shell, file, mode) {
    switch (mode.toLowerCase()) {
        case "absolute":
            return shell.translateAbsolutePath(getVaultAbsolutePath(app) + "/" + file.path);
        case "relative":
            return shell.translateRelativePath(file.path);
    }
}
/**
 * TODO: Consider creating a decorator class for TFile and moving this function to be a method in it.
 * @param file
 * @param with_dot
 */
function getFileExtension(file, with_dot) {
    const file_extension = file.extension;
    // Should the extension be given with or without a dot?
    if (with_dot) {
        // A preceding dot must be included.
        if (file_extension.length > 0) {
            // But only if the extension is not empty.
            return "." + file_extension;
        }
    }
    // No dot should be included, or the extension is empty
    return file_extension;
}
function getFileTags(app, file) {
    const cache = app.metadataCache.getFileCache(file);
    if (!cache) {
        throw new Error("Could not get metadata cache.");
    }
    // Get tags. May include duplicates, if a tag is defined multiple times in the same file.
    const tagsIncludingDuplicates = obsidian.getAllTags(cache) ?? []; // ?? [] = in case null is returned, convert it to an empty array. I have no clue in which situation this might happen. Maybe if the file does not contain any tags?
    // Iron out possible duplicates.
    const tagsWithoutDuplicates = uniqueArray(tagsIncludingDuplicates);
    // Remove preceding hash characters. E.g. #tag becomes tag
    tagsWithoutDuplicates.forEach((tag, index) => {
        tagsWithoutDuplicates[index] = tag.replace("#", "");
    });
    return tagsWithoutDuplicates;
}
function getFileYAMLValue(app, file, property_path, multipleValuesRequirement) {
    const error_messages = [];
    const property_parts = property_path.split(".");
    const acceptSingleValue = multipleValuesRequirement === false || multipleValuesRequirement === null;
    const acceptMultipleValues = multipleValuesRequirement === true || multipleValuesRequirement === null;
    // Validate all property names along the path
    property_parts.forEach((property_name) => {
        if (0 === property_name.length) {
            error_messages.push("YAML property '" + property_path + "' has an empty property name. Remove possible double dots or a preceding/trailing dot.");
        }
    });
    if (error_messages.length > 0) {
        // Failure in property name(s).
        return {
            success: false,
            errorMessages: error_messages,
        };
    }
    const frontmatter = app.metadataCache.getFileCache(file)?.frontmatter;
    // Check that a YAML section is available in the file
    if (undefined === frontmatter) {
        // No it ain't.
        error_messages.push("No YAML frontmatter section is defined for the current file.");
        return {
            success: false,
            errorMessages: error_messages,
        };
    }
    else {
        // A YAML section is available.
        // Read the property's value.
        return nested_read(property_parts, property_path, frontmatter);
    }
    /**
     * @param property_parts Property path split into parts (= property names). The deeper the nesting goes, the fewer values will be left in this array. This should always contain at least one part! If not, an Error is thrown.
     * @param property_path The original, whole property path string.
     * @param yaml_object
     * @return YAMLSingleValueResult | YAMLMultipleValuesResult
     */
    function nested_read(property_parts, property_path, yaml_object) {
        // Check that property_parts contains at least one part.
        if (property_parts.length === 0) {
            throw new Error("No more property parts to read!");
        }
        let property_name = property_parts.shift(); // as string: Tell TypeScript that the result is not undefined, because the array is not empty.
        // Check if the property name is a negative numeric index.
        if (property_name.match(/^-\d+$/u)) {
            // The property name is a negative number.
            // Check that yaml_object contains at least one element.
            const yaml_object_keys = Object.getOwnPropertyNames(yaml_object).filter(key => key !== "length"); // All _really custom_ yaml keys, not .length
            if (yaml_object_keys.length > 0) {
                // Check if yaml_object happens to be an indexed list.
                let is_indexed_list = true;
                yaml_object_keys.forEach((key) => {
                    if (!key.match(/^\d+$/u)) {
                        // At least one non-numeric key was found, so consider the object not to be an indexed list.
                        is_indexed_list = false;
                    }
                });
                if (is_indexed_list) {
                    // The object is an indexed list and property_name is a negative index number.
                    // Translate property_name to a positive index from the end of the list.
                    property_name = Math.max(0, // If a greatly negative index is used (e.g. -999), don't allow the new index to be negative again.
                    yaml_object_keys.length
                        + parseInt(property_name) // Although + is used, this will be a subtraction, because property_name is prefixed with a minus.
                    ).toString();
                }
            }
        }
        // Get a value
        const property_value = yaml_object[property_name];
        // Check if the value is either: not found, object, or literal.
        if (undefined === property_value) {
            // Property was not found.
            error_messages.push("YAML property '" + property_name + "' is not found.");
            return {
                success: false,
                errorMessages: error_messages,
            };
        }
        else if (null === property_value) {
            // Property is found, but has an empty value. Example:
            //   ---
            //   itemA: valueA
            //   itemB:
            //   itemC: valueC
            //   ---
            // Here `itemB` would have a null value.
            error_messages.push("YAML property '" + property_name + "' has a null value. Make sure the property is not accidentally left empty.");
            return {
                success: false,
                errorMessages: error_messages,
            };
        }
        else if ("object" === typeof property_value) {
            // The value is an object.
            // Check if we have still dot notation parts left in the property path.
            if (0 === property_parts.length) {
                // No dot notation parts are left.
                // Can the result contain multiple values?
                if (acceptMultipleValues) {
                    // Return multiple values - but only if they are all scalars, not nested objects.
                    if (Array.isArray(property_value)) {
                        if (isArrayOfScalars(property_value)) {
                            // All the values are scalars that can be concatenated into a single string (but the concatenation won't be done here).
                            return {
                                success: true,
                                multipleValues: property_value,
                            };
                        }
                        else {
                            // Some (or all) of the values are incompatible.
                            error_messages.push("YAML property '" + property_name + "' contains (at least) one value that is not a single scalar value or that is empty. E.g. nested lists are not supported.");
                            return {
                                success: false,
                                errorMessages: error_messages,
                            };
                        }
                    }
                    else {
                        // The property is an object with key-value pairs. This is not supported at the moment.
                        error_messages.push("YAML property '" + property_name + "' is a map object with key-value pairs. Reading multiple values from objects is not yet supported. Can the YAML be changed from \"key: value\" format to \"- list format\"?");
                        return {
                            success: false,
                            errorMessages: error_messages,
                        };
                    }
                }
                else {
                    // Freak out - a single value is expected.
                    const nested_elements_keys = Object.getOwnPropertyNames(property_value);
                    const multipleValuesTip = Array.isArray(property_value) ? " Or use the plural variable {{yaml_values:" + property_path + ":,}} to get multiple values." : ""; // Array.isArray() check can be removed when support for key-value maps is added to {{yaml_values}}.
                    if (nested_elements_keys.length > 0) {
                        error_messages.push("YAML property '" + property_name + "' contains a nested element with keys: " + nested_elements_keys.join(", ") + ". Use e.g. '" + property_path + "." + nested_elements_keys[0] + "' to get its value." + multipleValuesTip);
                    }
                    else {
                        error_messages.push("YAML property '" + property_name + "' contains a nested element. Use a property name that points to a literal value instead." + multipleValuesTip);
                    }
                    return {
                        success: false,
                        errorMessages: error_messages,
                    };
                }
            }
            else {
                // Dot notation path still has another property name left, so continue the hunt.
                return nested_read(property_parts, property_path, property_value);
            }
        }
        else {
            // The value is literal, i.e. a string or number.
            if (property_parts.length > 0) {
                error_messages.push("YAML property '" + property_name + "' gives already a literal value '" + property_value.toString() + "', but the argument '" + property_path + "' assumes the property would contain a nested element with the key '" + property_parts[0] + "'.");
                return {
                    success: false,
                    errorMessages: error_messages,
                };
            }
            else {
                if (acceptSingleValue) {
                    // The caller accepts a single value result.
                    return {
                        success: true,
                        singleValue: property_value.toString(),
                    };
                }
                else {
                    // The caller expects an array of values.
                    error_messages.push("YAML property '" + property_name + "' gives a single value '" + property_value.toString() + "', but a list of values was expected. Use the singular variable {{yaml_value:" + property_path + "}} if a single value is wanted.");
                    return {
                        success: false,
                        errorMessages: error_messages,
                    };
                }
            }
        }
    }
}
function isArrayOfScalars(object) {
    return Array.isArray(object) && object.every(item => isScalar(item, false));
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FileExtension extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "file_extension";
        this.help_text = "Gives the current file name's ending. Use {{file_extension:with-dot}} to include a preceding dot. If the extension is empty, no dot is added. {{file_extension:no-dot}} never includes a dot.";
    }
    async generateValue(shell, castedArguments) {
        return getFileExtension(this.getFileOrThrow(), castedArguments.dot === "with-dot");
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":no-dot}}",
                help_text: "Gives the current file name's ending without a preceding dot. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":with-dot}}",
                help_text: "Gives the current file name's ending with a preceding dot. If the extension is empty, no dot is included. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":no-dot}}",
                help_text: "Gives the current file name's ending without a preceding dot. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":with-dot}}",
                help_text: "Gives the current file name's ending with a preceding dot. If the extension is empty, no dot is included. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{file_extension:with-dot}}</strong> or <strong>{{file_extension:no-dot}}</strong>";
    }
}
Variable_FileExtension.parameters = {
    "dot": {
        options: ["with-dot", "no-dot"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FileName extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "file_name";
        this.help_text = "Gives the current file name with a file extension. If you need it without the extension, use {{title}} instead.";
    }
    async generateValue() {
        return this.getFileOrThrow().name;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FilePath extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "file_path";
        this.help_text = "Gives path to the current file, either as absolute from the root of the file system, or as relative from the root of the Obsidian vault.";
    }
    async generateValue(shell, castedArguments) {
        return getFilePath(this.app, shell, this.getFileOrThrow(), castedArguments.mode);
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the current file, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "Gives path to the current file, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the current file, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "Gives path to the current file, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{file_path:relative}}</strong> or <strong>{{file_path:absolute}}</strong>";
    }
}
Variable_FilePath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FileURI extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "file_uri";
        this.help_text = "Gives an Obsidian URI that opens the current file.";
    }
    async generateValue() {
        return this.plugin.getObsidianURI("open", {
            file: obsidian.normalizePath(this.getFileOrThrow().path), // Use normalizePath() instead of normalizePath2() because / should not be converted to \ on Windows because this is used as a URI, not as a file system path.
        });
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class FolderVariable extends FileVariable {
    getFolderOrThrow() {
        // Get current file's parent folder.
        const file = this.getFileOrThrow();
        const currentFolder = file.parent;
        if (!currentFolder) {
            // No parent folder.
            this.throw("The current file does not have a parent for some strange reason.");
        }
        return currentFolder;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FolderName extends FolderVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "folder_name";
        this.help_text = "Gives the current file's parent folder name, or a dot if the folder is the vault's root. No ancestor folders are included.";
    }
    async generateValue() {
        const folder = this.getFolderOrThrow();
        return folder.isRoot()
            ? "." // Return a dot instead of an empty string.
            : folder.name;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_FolderPath extends FolderVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "folder_path";
        this.help_text = "Gives path to the current file's parent folder, either as absolute from the root of the file system, or as relative from the root of the Obsidian vault.";
    }
    async generateValue(shell, castedArguments) {
        return getFolderPath(this.app, shell, this.getFolderOrThrow(), castedArguments.mode);
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the current file's parent folder, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "Gives path to the current file's parent folder, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the current file's parent folder, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "Gives path to the current file's parent folder, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{folder_path:relative}}</strong> or <strong>{{folder_path:absolute}}</strong>";
    }
}
Variable_FolderPath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_NewNoteFolderName extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "new_note_folder_name";
        this.help_text = "Gives the folder name for \"Default location for new notes\" (a setting in Obsidian). No ancestor folders are included.";
    }
    async generateValue() {
        const current_file = this.app.workspace.getActiveFile(); // Needed just in case new notes should be created in the same folder as the currently open file.
        const folder = this.app.fileManager.getNewFileParent(current_file ? current_file.path : ""); // If no file is open, use an empty string as instructed in .getNewFileParent()'s documentation.
        if (!folder) {
            this.throw("Cannot determine a folder name for new notes. Please create a discussion in GitHub."); // I guess this never happens.
        }
        // If the folder is the vault's root folder, return "." instead of " " (a space character). I don't know why the name is " " when the folder is root.
        return folder.isRoot()
            ? "."
            : folder.name;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_NewNoteFolderPath extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "new_note_folder_path";
        this.help_text = "Gives path to the \"Default location for new notes\" folder (a setting in Obsidian), either as absolute from the root of the file system, or as relative from the root of the Obsidian vault.";
    }
    async generateValue(shell, castedArguments) {
        const current_file = this.app.workspace.getActiveFile(); // Needed just in case new notes should be created in the same folder as the currently open file.
        const folder = this.app.fileManager.getNewFileParent(current_file ? current_file.path : ""); // If no file is open, use an empty string as instructed in .getNewFileParent()'s documentation.
        if (folder) {
            return getFolderPath(this.app, shell, folder, castedArguments.mode);
        }
        else {
            this.throw("Cannot determine a folder path for new notes. Please create a discussion in GitHub."); // I guess this never happens.
        }
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the \"Default location for new notes\" folder (a setting in Obsidian), absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "Gives path to the \"Default location for new notes\" folder (a setting in Obsidian), relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the \"Default location for new notes\" folder (a setting in Obsidian), absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "Gives path to the \"Default location for new notes\" folder (a setting in Obsidian), relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{folder_path:relative}}</strong> or <strong>{{folder_path:absolute}}</strong>";
    }
}
Variable_NewNoteFolderPath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Newline extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "newline";
        this.help_text = "Gives a \\n character. Used for testing line break escaping. An optional argument can be used to tell how many newlines are needed.";
    }
    async generateValue(shell, castedArguments) {
        // Return \n, possibly repeating it
        return "\n".repeat(castedArguments.count ?? 1);
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in debug mode.";
    }
}
Variable_Newline.parameters = {
    count: {
        type: "integer",
        required: false,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_NoteContent extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "note_content";
        this.help_text = "Gives the current note's content without YAML frontmatter. If you need YAML included, use {{file_content}} instead.";
    }
    async generateValue() {
        return await getFileContentWithoutYAML(this.app, this.getFileOrThrow());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_ObsidianAPIVersion extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "obsidian_api_version";
        this.help_text = "Gives Obsidian's API version, which follows the release cycle of the desktop application.";
    }
    async generateValue(shell, castedArguments) {
        if (undefined === castedArguments.part) {
            // Return the whole version.
            return obsidian.apiVersion;
        }
        // Return a part of the version.
        const versionPart = getVersionPart(obsidian.apiVersion, castedArguments.part);
        if (null === versionPart) {
            throw new Error("Obsidian API version (" + obsidian.apiVersion + ") does not contain the part: " + castedArguments.part);
        }
        return versionPart;
    }
    getAutocompleteItems() {
        const autocompleteItems = [
            {
                value: this.getFullName(),
                help_text: this.help_text + " " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, "major"),
                help_text: "Gives Obsidian's API version's first part, e.g. 1 from 1.5.3 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, "minor"),
                help_text: "Gives Obsidian's API version's middle part, e.g. 5 from 1.5.3 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, "patch"),
                help_text: "Gives Obsidian's API version's last part, e.g. 3 from 1.5.3 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
        Variable.supplementAutocompleteItems(autocompleteItems);
        return autocompleteItems;
    }
    getHelpName() {
        return "<strong>{{obsidian_api_version}}</strong> or <strong>{{obsidian_api_version:major|minor|patch}}</strong>";
    }
}
Variable_ObsidianAPIVersion.parameters = {
    part: {
        options: ["major", "minor", "patch"],
        required: false,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_OperatingSystem extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "operating_system";
        this.help_text = "Gives the current operating system's id code or human-readable name.";
    }
    async generateValue(shell, castedArguments) {
        switch (castedArguments.property) {
            case "id":
                return getOperatingSystem();
            case "name":
                return getCurrentPlatformName();
        }
    }
    getAutocompleteItems() {
        const autocompleteItems = [
            {
                value: this.getFullName(false, "id"),
                help_text: "Gives the current operating system's id code, i.e. \"darwin\" (= macOS), \"linux\", or \"win32\" (= Windows). Good for scripts as id comes from `navigator.platform` and is not likely to change. For a human-readable value, use :name instead." + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, "name"),
                help_text: "Gives the current operating system's human-readable name. As the OS names are defined in the SC plugin's source code, they might change if they need improving. If you need non-changing names, use :id instead." + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
        Variable.supplementAutocompleteItems(autocompleteItems);
        return autocompleteItems;
    }
    getHelpName() {
        return "<strong>{{operating_system:id}}</strong>, <strong>{{operating_system:name}}</strong>, <strong>{{operating_system:release}}</strong> or <strong>{{file_path:version}}</strong>";
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in debug mode.";
    }
}
Variable_OperatingSystem.parameters = {
    property: {
        options: ["id", "name"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Passthrough extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "passthrough";
        this.help_text = "Gives the same value that is passed as an argument. Used for testing special characters' escaping.";
    }
    async generateValue(shell, castedArguments) {
        // Simply return the argument that was received.
        return castedArguments.value;
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in debug mode.";
    }
}
Variable_Passthrough.parameters = {
    value: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Selection extends EditorVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "selection";
        this.help_text = "Gives the currently selected text.";
    }
    async generateValue() {
        // Check that we are able to get an editor
        const editor = this.getEditorOrThrow();
        // Check the view mode
        this.requireViewModeSource();
        // Good, the editor is in "source" mode, so it's possible to get a selection.
        if (editor.somethingSelected()) {
            return editor.getSelection();
        }
        this.throw("Nothing is selected. " + os.EOL + os.EOL + "(This error message was added in SC 0.18.0. Earlier the variable gave an empty text in this situation. If you want to restore the old behavior, go to SC settings, then to Variables tab, and define a default value for {{selection}}.)");
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> when something is selected in <em>Editing</em>/<em>Live preview</em> mode, <strong>not</strong> in <em>Reading</em> mode.";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_ShellCommandsPluginVersion extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "shell_commands_plugin_version";
        this.help_text = "Gives the plugin's version or settings structure version.";
    }
    async generateValue(shell, castedArguments) {
        let version;
        switch (castedArguments.subject) {
            case "plugin":
                version = this.plugin.getPluginVersion();
                break;
            case "settings":
                version = SC_Plugin.SettingsVersion;
                break;
        }
        if (undefined === castedArguments.part) {
            // Return the whole version.
            return version;
        }
        // Return a part of the version.
        const versionPart = getVersionPart(version, castedArguments.part);
        if (null === versionPart) {
            throw new Error("Shell commands " + castedArguments.subject + " version (" + version + ") does not contain the part: " + castedArguments.part);
        }
        return versionPart;
    }
    getAutocompleteItems() {
        const autocompleteItems = [
            {
                value: this.getFullName(false, ["plugin"]),
                help_text: "Gives the Shell commands plugin's version.",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["plugin", "major"]),
                help_text: "Gives the Shell commands plugin's version's first part, e.g. 0 from 0.22.1 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["plugin", "minor"]),
                help_text: "Gives the Shell commands plugin's version's middle part, e.g. 22 from 0.22.1 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["plugin", "patch"]),
                help_text: "Gives the Shell commands plugin's version's last part, e.g. 1 from 0.22.1 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["settings"]),
                help_text: "Gives the Shell commands' settings structure version, which is not always increased with new plugin versions.",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["settings", "major"]),
                help_text: "Gives the Shell commands' settings structure version's first part, e.g. 0 from 0.22.0 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["settings", "minor"]),
                help_text: "Gives the Shell commands' settings structure version's middle part, e.g. 22 from 0.22.0 .",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: this.getFullName(false, ["settings", "patch"]),
                help_text: "Gives the Shell commands' settings structure version's last part, e.g. 0 from 0.22.0 . It hardly ever differs from 0.",
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
        Variable.supplementAutocompleteItems(autocompleteItems);
        return autocompleteItems;
    }
    getHelpName() {
        return "<strong>{{shell_commands_plugin_version:plugin}}</strong>, <strong>{{shell_commands_plugin_version:plugin:major|minor|patch}}</strong>, <strong>{{shell_commands_plugin_version:settings}}</strong> or <strong>{{shell_commands_plugin_version:settings:major|minor|patch}}</strong>";
    }
}
Variable_ShellCommandsPluginVersion.parameters = {
    subject: {
        options: ["plugin", "settings"],
        required: true,
    },
    part: {
        options: ["major", "minor", "patch"],
        required: false,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Tags extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "tags";
        this.help_text = "Gives all tags defined in the current note. Replace the \"separator\" part with a comma, space or whatever characters you want to use as a separator between tags. A separator is always needed to be defined.";
    }
    async generateValue(shell, castedArguments) {
        return getFileTags(this.app, this.getFileOrThrow()).join(castedArguments.separator);
    }
}
Variable_Tags.parameters = {
    separator: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Title extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "title";
        this.help_text = "Gives the current file name without a file extension. If you need it with the extension, use {{file_name}} instead.";
    }
    async generateValue() {
        return this.getFileOrThrow().basename;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_VaultPath extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "vault_path";
        this.help_text = "Gives the Obsidian vault's absolute path from the root of the filesystem. This is the same that is used as a default working directory if you do not define one manually. If you define a working directory manually, this variable won't give you your manually defined directory, it always gives the vault's root directory.";
    }
    async generateValue(shell) {
        return shell.translateAbsolutePath(getVaultAbsolutePath(this.app));
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_Workspace extends Variable {
    constructor() {
        super(...arguments);
        this.variable_name = "workspace";
        this.help_text = "Gives the current workspace's name.";
        this.always_available = false;
    }
    async generateValue() {
        // Idea how to access the workspaces plugin is copied 2021-09-15 from https://github.com/Vinzent03/obsidian-advanced-uri/blob/f7ef80d5252481242e69496208e925874209f4aa/main.ts#L168-L179
        // @ts-ignore internalPlugins exists, although it's not in obsidian.d.ts. PRIVATE API
        const workspaces_plugin = this.app.internalPlugins?.plugins?.workspaces;
        if (!workspaces_plugin) {
            this.throw("Workspaces core plugin is not found for some reason. Please create a discussion in GitHub.");
        }
        else if (!workspaces_plugin.enabled) {
            this.throw("Workspaces core plugin is not enabled.");
        }
        const workspace_name = workspaces_plugin.instance?.activeWorkspace;
        if (!workspace_name) {
            this.throw("Could not figure out the current workspace's name. Probably you have not loaded a workspace. You can do it e.g. via \"Manage workspaces\" from the left side panel.");
        }
        // All ok
        return workspace_name;
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> when the Workspaces core plugin is enabled.";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_YAMLContent extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "yaml_content";
        this.help_text = "Gives the current note's YAML frontmatter. Dashes --- can be included or excluded.";
    }
    generateValue(shell, castedArguments) {
        return new Promise((resolve, reject) => {
            let file;
            try {
                file = this.getFileOrThrow();
            }
            catch (error) {
                // Need to catch here, because Variable.getValue()'s .catch() block won't be able to catch thrown errors,
                // it can only catch errors that were passed to reject().
                reject(error);
                return;
            }
            getFileYAML(this.app, file, "with-dashes" === castedArguments.withDashes).then((yamlContent) => {
                if (null === yamlContent) {
                    // No YAML frontmatter.
                    this.reject("The current file does not contain a YAML frontmatter.", reject);
                }
                else {
                    // Got a YAML frontmatter.
                    resolve(yamlContent);
                }
            });
        });
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Also, a YAML frontmatter section needs to be present.";
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":with-dashes}}",
                help_text: "Gives the current note's YAML frontmatter, wrapped between --- lines. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":no-dashes}}",
                help_text: "Gives the current note's YAML frontmatter, excluding top and bottom --- lines. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":with-dashes}}",
                help_text: "Gives the current note's YAML frontmatter, wrapped between --- lines." + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":no-dashes}}",
                help_text: "Gives the current note's YAML frontmatter, excluding top and bottom --- lines. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{yaml_content:with-dashes}}</strong> or <strong>{{yaml_content:no-dashes}}</strong>";
    }
}
Variable_YAMLContent.parameters = {
    withDashes: {
        options: ["with-dashes", "no-dashes"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_YAMLValues extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "yaml_values";
        this.help_text = "Reads a list of values from the current file's frontmatter. Takes a property name and separator as arguments. You can access nested properties with dot notation: property1.property2";
    }
    async generateValue(shell, castedArguments) {
        // We do have an active file
        const yamlResult = getFileYAMLValue(this.app, this.getFileOrThrow(), castedArguments.propertyName, true);
        if (yamlResult.success) {
            // The result is ok.
            return yamlResult.multipleValues.join(castedArguments.separator);
        }
        else {
            // The result contains error message(s).
            this.throw(yamlResult.errorMessages.join(" "));
        }
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Also, the given YAML property must exist in the file's frontmatter.";
    }
    getHelpName() {
        return "<strong>{{yaml_values:property:separator}}</strong>";
    }
}
Variable_YAMLValues.parameters = {
    propertyName: {
        type: "string",
        required: true,
    },
    separator: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_YAMLValue extends FileVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "yaml_value";
        this.help_text = "Reads a single value from the current file's frontmatter. Takes a property name as an argument. You can access nested properties with dot notation: property1.property2";
    }
    async generateValue(shell, castedArguments) {
        // We do have an active file
        const yamlResult = getFileYAMLValue(this.app, this.getFileOrThrow(), castedArguments.property_name, false);
        if (yamlResult.success) {
            // The result is ok.
            return yamlResult.singleValue;
        }
        else {
            // The result contains error message(s).
            this.throw(yamlResult.errorMessages.join(" "));
        }
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Also, the given YAML property must exist in the file's frontmatter.";
    }
    getHelpName() {
        return "<strong>{{yaml_value:property}}</strong>";
    }
}
Variable_YAMLValue.parameters = {
    property_name: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class EventVariable extends Variable {
    constructor() {
        super(...arguments);
        this.always_available = false;
    }
    /**
     * Every subclass should call this method in their generateValue() before generating a value. This method will throw
     * a VariableError if an incompatible SC_Event is tried to be used with this {{variable}}.
     *
     * @protected
     */
    requireCorrectEvent(sc_event) {
        // 1. Check generally that an event is happening.
        // (Maybe this check is not so important anymore, as sc_event is now received as a parameter instead of from a property, but check just in case.)
        if (!sc_event) {
            this.throw("This variable can only be used during events: " + this.getSummaryOfSupportedEvents());
        }
        // 2. Check particularly which event it is.
        if (!this.supportsSC_Event(sc_event.getClass())) {
            this.throw("This variable does not support event '" + sc_event.static().getTitle() + "'. Supported events: " + this.getSummaryOfSupportedEvents());
        }
    }
    supportsSC_Event(sc_event_class) {
        if (this.supported_sc_events === true) {
            // The variable supports all events.
            return true;
        }
        return this.supported_sc_events.contains(sc_event_class);
    }
    getSummaryOfSupportedEvents() {
        if (this.supported_sc_events === true) {
            // The variable supports all events.
            return "All events";
        }
        const sc_event_titles = [];
        this.supported_sc_events.forEach((sc_event_class) => {
            sc_event_titles.push(sc_event_class.getTitle());
        });
        return sc_event_titles.join(", ");
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in events: " + this.getSummaryOfSupportedEvents() + ".";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * Named SC_Event instead of just Event, because Event is a class in JavaScript.
 */
class SC_Event {
    constructor(plugin) {
        /**
         * If true, changing the enabled/disabled status of the event permits registering the event immediately, so it can activate
         * anytime. Usually true, but can be set to false if immediate registering tends to trigger the event unnecessarily.
         *
         * Events are always registered when loading the plugin, regardless of this property.
         * @protected
         */
        this.register_after_changing_settings = true;
        this.event_registrations = {};
        this.default_configuration = {
            enabled: false,
        };
        this.plugin = plugin;
        this.app = plugin.app;
        this.subclass_instance = this; // Stores a subclass reference, not a base class reference.
    }
    getClass() {
        return this.subclass_instance.constructor;
    }
    canRegisterAfterChangingSettings() {
        return this.register_after_changing_settings;
    }
    register(t_shell_command) {
        const event_reference = this._register(t_shell_command);
        if (event_reference) {
            this.plugin.registerEvent(event_reference);
            this.event_registrations[t_shell_command.getId()] = event_reference;
        }
    }
    unregister(t_shell_command) {
        // Check if an EventRef is available.
        if (undefined === this.event_registrations[t_shell_command.getId()]) {
            // The event was registered without an EventRef object.
            // Provide a TShellCommand to _unregister() so it can do a custom unregistering.
            this._unregister(t_shell_command);
        }
        else {
            // The event registration had created an EventRef object.
            // Provide the EventRef to _unregister() and forget it afterwards.
            this._unregister(this.event_registrations[t_shell_command.getId()]);
            delete this.event_registrations[t_shell_command.getId()];
        }
    }
    /**
     * Executes a shell command.
     * @param tShellCommand
     * @param parsingProcess SC_MenuEvent can use this to pass an already started ParsingProcess instance. If omitted, a new ParsingProcess will be created.
     */
    async trigger(tShellCommand, parsingProcess) {
        const debounce = this.static().debounce && tShellCommand.isDebouncingEnabled();
        debugLog(this.constructor.name + ": Event triggers executing shell command id " + tShellCommand.getId() + " " + (debounce ? "with" : "without") + " debouncing control.");
        if (debounce) {
            if (parsingProcess) {
                throw new Error("SC_Event.trigger() cannot be passed a ShellCommandParsingProcess object if SC_Event.debounce is true. This is just because passing ShellCommandParsingProcess to TShellCommand.executeWithDebouncing() is not implemented. It can be implemented later.");
            }
            await tShellCommand.executeWithDebouncing(this);
        }
        else {
            // Execute the shell command immediately.
            const executor = new ShellCommandExecutor(this.plugin, tShellCommand, this);
            await executor.doPreactionsAndExecuteShellCommand(parsingProcess);
        }
    }
    static getCode() {
        return this.event_code;
    }
    static getTitle() {
        return this.event_title;
    }
    /**
     * Creates a list of variables to the given container element. Each variable is a link to its documentation.
     *
     * @param container
     * @return A boolean indicating whether anything was created or not. Not all SC_Events utilise event variables.
     */
    createSummaryOfEventVariables(container) {
        let hasCreatedElements = false;
        this.getEventVariables().forEach((variable) => {
            if (hasCreatedElements) {
                container.insertAdjacentText("beforeend", ", ");
            }
            hasCreatedElements = true;
            variable.createDocumentationLinkElement(container);
        });
        return hasCreatedElements;
    }
    static canDebounce() {
        return this.debounce;
    }
    getEventVariables() {
        const event_variables = [];
        this.plugin.getVariables().forEach((variable) => {
            // Check if the variable is an EventVariable
            if (variable instanceof EventVariable) {
                // Yes it is.
                // Check if the variable supports this particular event.
                if (variable.supportsSC_Event(this.getClass())) {
                    // Yes it supports.
                    event_variables.push(variable);
                }
            }
        });
        return event_variables;
    }
    /**
     * Can be overridden in child classes that need custom settings fields.
     *
     * @param enabled
     */
    getDefaultConfiguration(enabled) {
        const configuration = cloneObject(this.default_configuration);
        configuration.enabled = enabled;
        return configuration;
    }
    getConfiguration(t_shell_command) {
        return t_shell_command.getEventConfiguration(this);
    }
    /**
     * Can be overridden in child classes to provide custom configuration fields for ShellCommandsExtraOptionsModal.
     *
     * @param extra_settings_container
     */
    createExtraSettingsFields(extra_settings_container, t_shell_command) {
        // Most classes do not define custom settings, so for those classes this method does not need to do anything.
    }
    /**
     * Returns all the TShellCommand instances that have enabled this event.
     */
    getTShellCommands() {
        const enabled_t_shell_commands = [];
        Object.values(this.plugin.getTShellCommands()).forEach((t_shell_command) => {
            // Check if this event has been enabled for the shell command.
            if (t_shell_command.isSC_EventEnabled(this.static().event_code)) {
                // Yes, it's enabled.
                enabled_t_shell_commands.push(t_shell_command);
            }
        });
        return enabled_t_shell_commands;
    }
    static() {
        return this.constructor;
    }
    /**
     * Child classes can override this to hook into a situation where a user has enabled an event in settings.
     *
     * @param t_shell_command The TShellCommand instance for which this SC_Event was enabled for.
     */
    onAfterEnabling(t_shell_command) {
        // If an SC_Event does not override this hook method, do nothing.
    }
    static getDocumentationLink() {
        return Documentation.events.folder + encodeURIComponent(this.event_title);
    }
}
/**
 * If true, this event's execution can be postponed or prevented completely by debouncing (only if it's
 * enabled in the executable shell command's configuration). Automatic events should have this set to true,
 * user-interaction events (i.e. menus) should have this set to false.
 *
 * @protected
 */
SC_Event.debounce = true;
const EventTypes = [
    "active-leaf-switched",
    "application-quit",
    "application-started",
    "caret-moved",
    "editor-menu-item",
    "every-n-seconds",
    "file-content-modified",
    "file-created",
    "file-deleted",
    "file-menu-item",
    "file-moved",
    "file-renamed",
    "folder-created",
    "folder-deleted",
    "folder-menu-item",
    "folder-moved",
    "folder-renamed",
]; // Keep in alphabetic order.
const EventCategories = [
    "application",
    "editor",
    "file",
    "folder",
    "menu",
    "time",
    "workspace",
];

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_WorkspaceEvent extends SC_Event {
    _register(t_shell_command) {
        // @ts-ignore TODO: Find a way to get a dynamic type for this.workspace_event .
        return this.app.workspace.on(this.workspace_event, this.getTrigger(t_shell_command));
    }
    _unregister(event_reference) {
        this.app.workspace.offref(event_reference);
    }
    getTrigger(t_shell_command) {
        return async (...parameters /* Need to have this ugly parameter thing so that subclasses can define their own parameters. */) => await this.trigger(t_shell_command);
    }
    getCategory() {
        return "workspace";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_MenuEvent extends SC_WorkspaceEvent {
    async addTShellCommandToMenu(t_shell_command, menu) {
        const debugLogBaseMessage = this.constructor.name + ".addTShellCommandToMenu(): ";
        // Create the menu item as soon as possible. (If it's created after 'await parsing_process.process()' below, it won't be shown in the menu for some reason, at least in Obsidian 0.16.1).
        debugLog(debugLogBaseMessage + "Creating a menu item. Container menu: " + menu.constructor.name);
        menu.addItem((menuItem) => {
            let parsing_process;
            // Menu item creation has to happen synchronously - at least on macOS, so:
            // 1. Set first all menu item properties that can be set already:
            //    - A preliminary title: Use shell command alias WITHOUT parsing any possible variables.
            //    - Icon (if defined for the shell command)
            //    - A click handler
            // 2. Then call an asynchronous function that will parse possible variables in the menu title and UPDATE the title. The updating only works on some systems. Systems that will not support the delayed update, will show the first, unparsed title. It's better than nothing.
            // 1. Set properties early.
            let title = t_shell_command.getAliasOrShellCommand(); // May contain unparsed variables.
            debugLog(debugLogBaseMessage + "Setting a preliminary menu title (possible variables are not parsed yet): ", title);
            menuItem.setTitle(title);
            menuItem.setIcon(t_shell_command.getIconId()); // Icon id can be null.
            menuItem.onClick(async () => {
                debugLog(debugLogBaseMessage + "Menu item '" + title + "' is clicked. Will execute shell command id " + t_shell_command.getId() + ".");
                await this.trigger(t_shell_command, parsing_process);
            });
            // 2. Parse variables asynchronously.
            if (this.plugin.settings.preview_variables_in_command_palette) {
                // Start a parsing process ASYNCHRONOUSLY.
                debugLog(debugLogBaseMessage + "Will parse menu title: " + title);
                (async () => {
                    parsing_process = t_shell_command.createParsingProcess(this);
                    if (await parsing_process.process()) {
                        // Parsing succeeded.
                        const parsing_results = parsing_process.getParsingResults();
                        const aliasParsingResult = parsing_results["alias"]; // as ParsingResult: Tells TypeScript that the object exists.
                        const unwrappedShellCommandParsingResult = parsing_results.shellCommandContent; // as ParsingResult: Tells TypeScript that the object exists.
                        title = aliasParsingResult.parsed_content || unwrappedShellCommandParsingResult.parsed_content; // Try to use a parsed alias, but if no alias is available, use a (short, unwrapped) parsed shell command instead. as string = parsed shell command always exist when the parsing itself has succeeded.
                        debugLog(debugLogBaseMessage + "Menu title parsing succeeded. Will use title: " + title);
                        menuItem.setTitle(title);
                    }
                    else {
                        // If parsing process fails, the failed process can be passed to this.trigger(). The execution will eventually be cancelled and error messages displayed (assuming user clicks the menu item to execute the shell command, AND if displaying errors is allowed in the shell command's settings).
                        // Keep the title set in phase 1 as-is. I.e. the title shows unparsed variables.
                        debugLog(debugLogBaseMessage + "Menu title parsing failed. Error message(s): ", ...parsing_process.getErrorMessages());
                    }
                })().then(); // Note: no waiting. If you add code below, it will evaluate before the above variable parsing finishes.
                // For the future: If Obsidian will make Menu.addItem() support async callback functions, remove the above '.then()' and use an 'await' instead to make this function properly signal Obsidian when the menu title generation process has finished. Follow this discussion: https://forum.obsidian.md/t/menu-additem-support-asynchronous-callback-functions/52870
            }
            else {
                debugLog(debugLogBaseMessage + "Alias parsing is disabled in settings.");
            }
        });
    }
    getCategory() {
        return "menu";
    }
}
SC_MenuEvent.debounce = false;

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_AbstractFileMenuEvent extends SC_MenuEvent {
    constructor() {
        super(...arguments);
        this.workspace_event = "file-menu";
    }
    getTrigger(t_shell_command) {
        return async (menu, file, source, leaf) => {
            // Check that it's the correct menu: if the SC_Event requires a file menu, 'file' needs to be a TFile, otherwise it needs to be a TFolder.
            if ((this.file_or_folder === "folder" && file instanceof obsidian.TFolder) || (this.file_or_folder === "file" && file instanceof obsidian.TFile)) {
                // The menu is correct.
                // File/folder for declareExtraVariables()
                switch (this.file_or_folder) {
                    case "file":
                        this.file = file;
                        break;
                    case "folder":
                        this.folder = file;
                        break;
                }
                await this.addTShellCommandToMenu(t_shell_command, menu);
            }
        };
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return this.file_or_folder + "-menu-item";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FileMenu extends SC_AbstractFileMenuEvent {
    constructor() {
        super(...arguments);
        this.file_or_folder = "file";
    }
    getFile() {
        return this.file;
    }
    getFolder() {
        if (!this.file.parent) {
            throw new Error("The event file does not have a parent for some strange reason.");
        }
        return this.file.parent;
    }
}
SC_Event_FileMenu.event_code = "file-menu";
SC_Event_FileMenu.event_title = "File menu";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_VaultEvent extends SC_Event {
    _register(t_shell_command) {
        // @ts-ignore TODO: Find a way to get a dynamic type for this.vault_event .
        return this.app.vault.on(this.vault_event, this.getTrigger(t_shell_command));
    }
    _unregister(event_reference) {
        this.app.vault.offref(event_reference);
    }
    getTrigger(t_shell_command) {
        return async (file, ...extra_arguments /* Needed for SC_Event_FileRenamed and SC_Event_FolderRenamed to be able to define an additional parameter.*/) => {
            // Check that it's the correct type of file: if the SC_Event requires a file, 'file' needs to be a TFile, otherwise it needs to be a TFolder.
            if ((this.file_or_folder === "folder" && file instanceof obsidian.TFolder) || (this.file_or_folder === "file" && file instanceof obsidian.TFile)) {
                // The file type is correct.
                // File/folder for declareExtraVariables()
                switch (this.file_or_folder) {
                    case "file":
                        this.file = file;
                        break;
                    case "folder":
                        this.folder = file;
                        break;
                }
                await this.trigger(t_shell_command);
            }
        };
    }
    /**
     * This should only be called if file_or_folder is "file"!
     */
    getFile() {
        return this.file;
    }
    /**
     * This can be called whether file_or_folder is "file" or "folder".
     */
    getFolder() {
        switch (this.file_or_folder) {
            case "file":
                if (!this.file.parent) {
                    throw new Error("The event file does not have a parent for some strange reason.");
                }
                return this.file.parent;
            case "folder":
                return this.folder;
        }
    }
    getCategory() {
        return this.file_or_folder;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FileCreated extends SC_VaultEvent {
    constructor() {
        super(...arguments);
        this.vault_event = "create";
        this.file_or_folder = "file";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "file-created";
    }
}
SC_Event_FileCreated.event_code = "file-created";
SC_Event_FileCreated.event_title = "File created";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FileContentModified extends SC_VaultEvent {
    constructor() {
        super(...arguments);
        this.vault_event = "modify";
        this.file_or_folder = "file";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "file-content-modified";
    }
}
SC_Event_FileContentModified.event_code = "file-content-modified";
SC_Event_FileContentModified.event_title = "File content modified";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FileDeleted extends SC_VaultEvent {
    constructor() {
        super(...arguments);
        this.vault_event = "delete";
        this.file_or_folder = "file";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "file-deleted";
    }
}
SC_Event_FileDeleted.event_code = "file-deleted";
SC_Event_FileDeleted.event_title = "File deleted";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_VaultMoveOrRenameEvent extends SC_VaultEvent {
    constructor() {
        super(...arguments);
        this.vault_event = "rename";
    }
    getTrigger(t_shell_command) {
        // Get a trigger from the parent class (SC_VaultEvent).
        const trigger = super.getTrigger(t_shell_command);
        return async (abstract_file, old_relative_path) => {
            // Detect if the file/folder was moved or renamed.
            // If the file/folder name has stayed the same, conclude that the file has been MOVED, not renamed. Otherwise, conclude the opposite.
            const old_file_name = extractFileName(old_relative_path);
            const new_file_name = abstract_file.name;
            const event_type = (old_file_name === new_file_name) ? "move" : "rename"; // Tells what really happened. this.move_or_rename tells what is the condition for the event to trigger.
            // Only proceed the triggering, if the determined type equals the one defined by the event class.
            if (event_type === this.move_or_rename) {
                // The event type is correct.
                // File and folder for declareExtraVariables()
                switch (this.file_or_folder) {
                    case "file":
                        this.file_old_relative_path = old_relative_path;
                        this.folder_old_relative_path = extractFileParentPath(old_relative_path);
                        break;
                    case "folder":
                        this.folder_old_relative_path = old_relative_path;
                        break;
                }
                // Call the normal trigger function.
                await trigger(abstract_file);
            }
        };
    }
    getFolderOldRelativePath() {
        return this.folder_old_relative_path;
    }
    getFileOldRelativePath() {
        return this.file_old_relative_path;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FileRenamed extends SC_VaultMoveOrRenameEvent {
    constructor() {
        super(...arguments);
        this.move_or_rename = "rename";
        this.file_or_folder = "file";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "file-renamed";
    }
}
SC_Event_FileRenamed.event_code = "file-renamed";
SC_Event_FileRenamed.event_title = "File renamed";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FileMoved extends SC_VaultMoveOrRenameEvent {
    constructor() {
        super(...arguments);
        this.move_or_rename = "move";
        this.file_or_folder = "file";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "file-moved";
    }
}
SC_Event_FileMoved.event_code = "file-moved";
SC_Event_FileMoved.event_title = "File moved";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFileContent extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_file_content";
        this.help_text = "Gives the event related file's content, including YAML frontmatter. If you need YAML excluded, use {{event_note_content}} instead.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        // Retrieve file content.
        return await app.vault.read(sc_event.getFile());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFileExtension extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_file_extension";
        this.help_text = "Gives the event related file name's ending. Use {{event_file_extension:with-dot}} to include a preceding dot. If the extension is empty, no dot is added. {{event_file_extension:no-dot}} never includes a dot.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        return getFileExtension(sc_event.getFile(), castedArguments.dot === "with-dot");
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":no-dot}}",
                help_text: "Gives the event related file name's ending without a preceding dot. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":with-dot}}",
                help_text: "Gives the event related file name's ending with a preceding dot. If the extension is empty, no dot is included. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":no-dot}}",
                help_text: "Gives the event related file name's ending without a preceding dot. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":with-dot}}",
                help_text: "Gives the event related file name's ending with a preceding dot. If the extension is empty, no dot is included. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{event_file_extension:with-dot}}</strong> or <strong>{{event_file_extension:no-dot}}</strong>";
    }
}
Variable_EventFileExtension.parameters = {
    "dot": {
        options: ["with-dot", "no-dot"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFileName extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_file_name";
        this.help_text = "Gives the event related file name with a file extension. If you need it without the extension, use {{event_title}} instead.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        return sc_event.getFile().name;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFilePath extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_file_path";
        this.help_text = "Gives path to the event related file, either as absolute from the root of the file system, or as relative from the root of the Obsidian vault.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        return getFilePath(this.app, shell, sc_event.getFile(), castedArguments.mode);
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the event related file, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "Gives path to the event related file, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "Gives path to the event related file, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "Gives path to the event related file, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{event_file_path:relative}}</strong> or <strong>{{event_file_path:absolute}}</strong>";
    }
}
Variable_EventFilePath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFileURI extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_file_uri";
        this.help_text = "Gives an Obsidian URI that opens the event related file.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        const file = sc_event.getFile();
        return this.plugin.getObsidianURI("open", {
            file: obsidian.normalizePath(file.path), // Use normalizePath() instead of normalizePath2() because / should not be converted to \ on Windows because this is used as a URI, not as a file system path.
        });
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FolderMenu extends SC_AbstractFileMenuEvent {
    constructor() {
        super(...arguments);
        this.file_or_folder = "folder";
    }
    getFolder() {
        return this.folder;
    }
}
SC_Event_FolderMenu.event_code = "folder-menu";
SC_Event_FolderMenu.event_title = "Folder menu";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FolderCreated extends SC_VaultEvent {
    constructor() {
        super(...arguments);
        this.vault_event = "create";
        this.file_or_folder = "folder";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "folder-created";
    }
}
SC_Event_FolderCreated.event_code = "folder-created";
SC_Event_FolderCreated.event_title = "Folder created";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FolderDeleted extends SC_VaultEvent {
    constructor() {
        super(...arguments);
        this.vault_event = "delete";
        this.file_or_folder = "folder";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "folder-deleted";
    }
}
SC_Event_FolderDeleted.event_code = "folder-deleted";
SC_Event_FolderDeleted.event_title = "Folder deleted";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FolderRenamed extends SC_VaultMoveOrRenameEvent {
    constructor() {
        super(...arguments);
        this.move_or_rename = "rename";
        this.file_or_folder = "folder";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "folder-renamed";
    }
}
SC_Event_FolderRenamed.event_code = "folder-renamed";
SC_Event_FolderRenamed.event_title = "Folder renamed";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_FolderMoved extends SC_VaultMoveOrRenameEvent {
    constructor() {
        super(...arguments);
        this.move_or_rename = "move";
        this.file_or_folder = "folder";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "folder-moved";
    }
}
SC_Event_FolderMoved.event_code = "folder-moved";
SC_Event_FolderMoved.event_title = "Folder moved";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFolderName extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_folder_name";
        this.help_text = "File events: Gives the event related file's parent folder name. Folder events: Gives the selected folder's name. Gives a dot if the folder is the vault's root. No ancestor folders are included.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FolderMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
            SC_Event_FolderCreated,
            SC_Event_FolderDeleted,
            SC_Event_FolderMoved,
            SC_Event_FolderRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        const folder = sc_event.getFolder();
        return folder.isRoot()
            ? "." // Return a dot instead of an empty string.
            : folder.name;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventFolderPath extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_folder_path";
        this.help_text = "File events: Gives path to the event related file's parent folder. Folder events: Gives path to the event related folder. The path is either absolute from the root of the file system, or relative from the root of the Obsidian vault.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FolderMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
            SC_Event_FolderCreated,
            SC_Event_FolderDeleted,
            SC_Event_FolderMoved,
            SC_Event_FolderRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        return getFolderPath(this.app, shell, sc_event.getFolder(), castedArguments.mode);
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "File events: Gives path to the event related file's parent folder. Folder events: Gives path to the event related folder. The path is absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "File events: Gives path to the event related file's parent folder. Folder events: Gives path to the event related folder. The path is relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "File events: Gives path to the event related file's parent folder. Folder events: Gives path to the event related folder. The path is absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "File events: Gives path to the event related file's parent folder. Folder events: Gives path to the event related folder. The path is relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{event_folder_path:relative}}</strong> or <strong>{{event_folder_path:absolute}}</strong>";
    }
}
Variable_EventFolderPath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventNoteContent extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_note_content";
        this.help_text = "Gives the event related file's content without YAML frontmatter. If you need YAML included, use {{event_file_content}} instead.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        return await getFileContentWithoutYAML(this.app, sc_event.getFile());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventOldFileName extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_old_file_name";
        this.help_text = "Gives the renamed file's old name with a file extension. If you need it without the extension, use {{event_old_title}} instead.";
        this.supported_sc_events = [
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        return extractFileName(sc_event.getFileOldRelativePath(), true);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventOldFilePath extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_old_file_path";
        this.help_text = "Gives the renamed/moved file's old path, either as absolute from the root of the file system, or as relative from the root of the Obsidian vault.";
        this.supported_sc_events = [
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        const file_old_relative_path = sc_event.getFileOldRelativePath();
        switch (castedArguments.mode.toLowerCase()) {
            case "relative":
                return shell.translateRelativePath(file_old_relative_path);
            case "absolute":
                return shell.translateAbsolutePath(getVaultAbsolutePath(this.app) + "/" + file_old_relative_path);
        }
        this.throw("Unrecognized mode parameter: " + castedArguments.mode);
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "Gives the renamed/moved file's old path, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "Gives the renamed/moved file's old path, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "Gives the renamed/moved file's old path, absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "Gives the renamed/moved file's old path, relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{event_file_path:relative}}</strong> or <strong>{{event_file_path:absolute}}</strong>";
    }
}
Variable_EventOldFilePath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventOldFolderName extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_old_folder_name";
        this.help_text = "File events: Gives the moved file's old parent folder's name. Folder events: Gives the renamed folder's old name.";
        this.supported_sc_events = [
            SC_Event_FileMoved,
            SC_Event_FolderRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        return extractFileName(sc_event.getFolderOldRelativePath());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventOldFolderPath extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_old_folder_path";
        this.help_text = "File events: Gives the moved file's old parent folder's path. Folder events: Gives the renamed/moved folder's old path. The path is either as absolute from the root of the file system, or as relative from the root of the Obsidian vault.";
        this.supported_sc_events = [
            SC_Event_FileMoved,
            SC_Event_FolderMoved,
            SC_Event_FolderRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        const folder_old_relative_path = sc_event.getFolderOldRelativePath();
        switch (castedArguments.mode.toLowerCase()) {
            case "relative":
                return shell.translateRelativePath(folder_old_relative_path);
            case "absolute":
                return shell.translateAbsolutePath(getVaultAbsolutePath(this.app) + "/" + folder_old_relative_path);
        }
        this.throw("Unrecognized mode parameter: " + castedArguments.mode);
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":absolute}}",
                help_text: "File events: Gives the moved file's old parent folder's path. Folder events: Gives the renamed/moved folder's old path. The path is absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":relative}}",
                help_text: "File events: Gives the moved file's old parent folder's path. Folder events: Gives the renamed/moved folder's old path. The path is relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":absolute}}",
                help_text: "File events: Gives the moved file's old parent folder's path. Folder events: Gives the renamed/moved folder's old path. The path is absolute from the root of the file system. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":relative}}",
                help_text: "File events: Gives the moved file's old parent folder's path. Folder events: Gives the renamed/moved folder's old path. The path is relative from the root of the Obsidian vault. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{event_file_path:relative}}</strong> or <strong>{{event_file_path:absolute}}</strong>";
    }
}
Variable_EventOldFolderPath.parameters = {
    mode: {
        options: ["absolute", "relative"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventOldTitle extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_old_title";
        this.help_text = "Gives the renamed file's old name without a file extension. If you need it with the extension, use {{event_old_file_name}} instead.";
        this.supported_sc_events = [
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        return extractFileName(sc_event.getFileOldRelativePath(), false);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventTags extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_tags";
        this.help_text = "Gives all tags defined in the event related note. Replace the \"separator\" part with a comma, space or whatever characters you want to use as a separator between tags. A separator is always needed to be defined.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        const file = sc_event.getFile();
        return getFileTags(this.app, file).join(castedArguments.separator);
    }
}
Variable_EventTags.parameters = {
    separator: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventTitle extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_title";
        this.help_text = "Gives the event related file name without a file extension. If you need it with the extension, use {{event_file_name}} instead.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, argumentsAreNotUsed, sc_event) {
        this.requireCorrectEvent(sc_event);
        return sc_event.getFile().basename;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventType extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_type";
        this.help_text = "Tells which event was triggered.";
        /**
         * This variable is available in all events.
         * @protected
         */
        this.supported_sc_events = true;
    }
    async generateValue(shell, castedArguments, sc_event) {
        // Check that an event was triggered, i.e. the execution does not happen via command palette or any other non-event way.
        this.requireCorrectEvent(sc_event);
        if (castedArguments.mode === "category") {
            // Get event category.
            return sc_event.getCategory();
        }
        else {
            // Get event type.
            return sc_event.getType();
        }
    }
    getAutocompleteItems() {
        const autocompleteItems = [
            {
                value: "{{" + this.variable_name + "}}",
                help_text: "Gives a name of the triggered event. Possible values: <code>" + EventTypes.join("</code> , <code>") + "</code>. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":category}}",
                help_text: "Gives a category of the triggered event. Multiple events can share the same category. Possible values: <code>" + EventCategories.join("</code> , <code>") + "</code>. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
        Variable.supplementAutocompleteItems(autocompleteItems);
        return autocompleteItems;
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in any event.";
    }
    getHelpName() {
        return "<strong>{{event_type}}</strong> or <strong>{{event_type:category}}</strong>";
    }
}
Variable_EventType.parameters = {
    mode: {
        options: ["category"],
        required: false,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventYAMLContent extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_yaml_content";
        this.help_text = "Gives the event related note's YAML frontmatter. Dashes --- can be included or excluded.";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    generateValue(shell, castedArguments, sc_event) {
        return new Promise((resolve, reject) => {
            try {
                this.requireCorrectEvent(sc_event);
            }
            catch (error) {
                // Need to catch here, because Variable.getValue()'s .catch() block won't be able to catch thrown errors,
                // it can only catch errors that were passed to reject().
                reject(error);
                return;
            }
            getFileYAML(this.app, sc_event.getFile(), castedArguments.withDashes === "with-dashes").then((yamlContent) => {
                if (null === yamlContent) {
                    // No YAML frontmatter.
                    this.reject("The event related file does not contain a YAML frontmatter.", reject);
                }
                else {
                    // Got a YAML frontmatter.
                    resolve(yamlContent);
                }
            });
        });
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Also, a YAML frontmatter section needs to be present.";
    }
    getAutocompleteItems() {
        return [
            // Normal variables
            {
                value: "{{" + this.variable_name + ":with-dashes}}",
                help_text: "Gives the event related note's YAML frontmatter, wrapped between --- lines. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{" + this.variable_name + ":no-dashes}}",
                help_text: "Gives the event related note's YAML frontmatter, excluding top and bottom --- lines. " + this.getAvailabilityText(),
                group: "Variables",
                type: "normal-variable",
                documentationLink: this.getDocumentationLink(),
            },
            // Unescaped variables
            {
                value: "{{!" + this.variable_name + ":with-dashes}}",
                help_text: "Gives the event related note's YAML frontmatter, wrapped between --- lines." + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
            {
                value: "{{!" + this.variable_name + ":no-dashes}}",
                help_text: "Gives the event related note's YAML frontmatter, excluding top and bottom --- lines. " + this.getAvailabilityText(),
                group: "Variables",
                type: "unescaped-variable",
                documentationLink: this.getDocumentationLink(),
            },
        ];
    }
    getHelpName() {
        return "<strong>{{event_yaml_content:with-dashes}}</strong> or <strong>{{event_yaml_content:no-dashes}}</strong>";
    }
}
Variable_EventYAMLContent.parameters = {
    withDashes: {
        options: ["with-dashes", "no-dashes"],
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventYAMLValues extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_yaml_values";
        this.help_text = "Reads a list of values from the event related file's frontmatter. Takes a property name and separator as arguments. You can access nested properties with dot notation: property1.property2";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        const yamlResult = getFileYAMLValue(this.app, sc_event.getFile(), castedArguments.propertyName, true);
        if (yamlResult.success) {
            // The result is ok.
            return yamlResult.multipleValues.join(castedArguments.separator);
        }
        else {
            // The result contains error message(s).
            this.throw(yamlResult.errorMessages.join(" "));
        }
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Also, the given YAML property must exist in the file's frontmatter.";
    }
    getHelpName() {
        return "<strong>{{event_yaml_values:property:separator}}</strong>";
    }
}
Variable_EventYAMLValues.parameters = {
    propertyName: {
        type: "string",
        required: true,
    },
    separator: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_EventYAMLValue extends EventVariable {
    constructor() {
        super(...arguments);
        this.variable_name = "event_yaml_value";
        this.help_text = "Reads a single value from the event related file's frontmatter. Takes a property name as an argument. You can access nested properties with dot notation: property1.property2";
        this.supported_sc_events = [
            SC_Event_FileMenu,
            SC_Event_FileCreated,
            SC_Event_FileContentModified,
            SC_Event_FileDeleted,
            SC_Event_FileMoved,
            SC_Event_FileRenamed,
        ];
    }
    async generateValue(shell, castedArguments, sc_event) {
        this.requireCorrectEvent(sc_event);
        const yamlResult = getFileYAMLValue(this.app, sc_event.getFile(), castedArguments.property_name, false);
        if (yamlResult.success) {
            // The result is ok.
            return yamlResult.singleValue;
        }
        else {
            // The result contains error message(s).
            this.throw(yamlResult.errorMessages.join(" "));
        }
    }
    getAvailabilityText() {
        return super.getAvailabilityText() + " Also, the given YAML property must exist in the file's frontmatter.";
    }
    getHelpName() {
        return "<strong>{{event_yaml_value:property}}</strong>";
    }
}
Variable_EventYAMLValue.parameters = {
    property_name: {
        type: "string",
        required: true,
    },
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
// Event variables above. Keep in alphabetic order.
function loadVariables(plugin) {
    const variables = new VariableSet([]);
    // Load CustomVariables
    // Do this before loading built-in variables so that these user-defined variables will appear first in all lists containing variables.
    plugin.getCustomVariableInstances().forEach((custom_variable_instance) => {
        variables.add(custom_variable_instance.createCustomVariable());
    });
    // Load built-in variables.
    const built_in_variables = [
        // Normal variables
        new Variable_CaretParagraph(plugin),
        new Variable_CaretPosition(plugin),
        new Variable_Clipboard(plugin),
        new Variable_Date(plugin),
        new Variable_Environment(plugin),
        new Variable_FileContent(plugin),
        new Variable_FileExtension(plugin),
        new Variable_FileName(plugin),
        new Variable_FilePath(plugin),
        new Variable_FileURI(plugin),
        new Variable_FolderName(plugin),
        new Variable_FolderPath(plugin),
        new Variable_NewNoteFolderName(plugin),
        new Variable_NewNoteFolderPath(plugin),
        new Variable_NoteContent(plugin),
        new Variable_ObsidianAPIVersion(plugin),
        // Variable_Output is not loaded here, because it's only used in OutputWrappers.
        new Variable_Selection(plugin),
        new Variable_ShellCommandsPluginVersion(plugin),
        new Variable_Tags(plugin),
        new Variable_Title(plugin),
        new Variable_VaultPath(plugin),
        new Variable_Workspace(plugin),
        new Variable_YAMLContent(plugin),
        new Variable_YAMLValue(plugin),
        new Variable_YAMLValues(plugin),
        // Event variables
        new Variable_EventFileContent(plugin),
        new Variable_EventFileExtension(plugin),
        new Variable_EventFileName(plugin),
        new Variable_EventFilePath(plugin),
        new Variable_EventFileURI(plugin),
        new Variable_EventFolderName(plugin),
        new Variable_EventFolderPath(plugin),
        new Variable_EventNoteContent(plugin),
        new Variable_EventOldFileName(plugin),
        new Variable_EventOldFilePath(plugin),
        new Variable_EventOldFolderName(plugin),
        new Variable_EventOldFolderPath(plugin),
        new Variable_EventOldTitle(plugin),
        new Variable_EventTags(plugin),
        new Variable_EventTitle(plugin),
        new Variable_EventType(plugin),
        new Variable_EventYAMLContent(plugin),
        new Variable_EventYAMLValue(plugin),
        new Variable_EventYAMLValues(plugin),
    ];
    if (DEBUG_ON) {
        // Variables that are only designed for 'Shell commands test suite'.
        built_in_variables.push(new Variable_Newline(plugin), new Variable_OperatingSystem(plugin), // TODO: Add to hidden settings documentation.
        new Variable_Passthrough(plugin));
    }
    for (const built_in_variable of built_in_variables) {
        // JavaScript's Set does not have a method to add multiple items at once, so need to iterate them and add one-by-one.
        variables.add(built_in_variable);
    }
    return variables;
}
/**
 * TODO: Check if VariableSet usages could be refactored to VariableMaps?
 */
class VariableSet extends Set {
}
class VariableMap extends Map {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * @param plugin
 * @param content
 * @param shell Used 1) to determine how to escape special characters in variable values (if escapeVariables is true), and 2) do correct path normalization (for variables that return file system paths).
 * @param escapeVariables If true, special characters in variable values are quoted (but this might be prevented if a variable starts with {{! instead of {{ ). If false, dno escaping is ever done.
 * @param t_shell_command Will only be used to read default value configurations. Can be null if no TShellCommand is available, but then no default values can be accessed.
 * @param sc_event Use undefined, if parsing is not happening during an event.
 * @param variables If you want to parse only a certain set of variables, define them in this parameter. If this is omitted, all variables will be parsed.
 * @param raw_value_augmenter A callback that will be called before every substitution. Allows modifying or completely changing the resulted variable values.
 * @param escaped_value_augmenter Same as raw_value_augmenter, but called after escaping the value. Can be used to for example wrap values in html elements for displaying purposes.
 * @return ParsingResult
 */
async function parseVariables(plugin, content, shell, escapeVariables, t_shell_command, sc_event, variables = plugin.getVariables(), raw_value_augmenter = null, escaped_value_augmenter = null) {
    debugLog("parseVariables(): Starting to parse " + content + " with " + variables.size + " variables.");
    // Initialize a parsing result object
    const parsing_result = {
        original_content: content,
        parsed_content: content,
        succeeded: false,
        error_messages: [],
        count_parsed_variables: 0,
    };
    for (const variable of variables) {
        const pattern = getVariableRegExp(variable);
        const parameter_names = variable.getParameterNames();
        let argument_matches;
        while ((argument_matches = pattern.exec(content)) !== null) {
            // Count how many times any variables have appeared.
            parsing_result.count_parsed_variables++;
            // Remove stuff that should not be iterated in a later loop.
            /** Need to prefix with _ because JavaScript reserves the variable name 'arguments'. */
            const _arguments = argument_matches.filter((value /* Won't be used */, key) => {
                return "number" === typeof key;
                // This leaves out for example the following non-numeric keys (and their values):
                // - "groups"
                // - "index"
                // - "input"
                // In the future, there can also come more elements that will be skipped. E.g. "indices". See: https://github.com/nothingislost/obsidian-dynamic-highlights/issues/25#issuecomment-1038563990 (referenced 2022-02-22).
            });
            // Get the {{variable}} string that will be substituted (= replaced with the actual value of the variable).
            const substitute = _arguments.shift(); // '_arguments[0]' contains the whole match, not just an argument. Get it and remove it from '_arguments'. 'as string' is used to tell TypeScript that _arguments[0] is always defined.
            // Iterate all arguments
            const presentArguments = {};
            for (const i in _arguments) {
                // Check that the argument is not omitted. It can be omitted (= undefined), if the parameter is optional.
                if (undefined !== _arguments[i]) {
                    // The argument is present.
                    const argument = _arguments[i].slice(1); // .slice(1): Remove a preceding :
                    const parameter_name = parameter_names[i];
                    presentArguments[parameter_name] = argument;
                }
            }
            // Should the variable's value be escaped? (Usually yes).
            let escapeCurrentVariable = escapeVariables;
            if (doesOccurrenceDenyEscaping(substitute)) {
                // The variable usage begins with {{! instead of {{
                // This means the variable's value should NOT be escaped.
                escapeCurrentVariable = false;
            }
            // Render the variable
            const variable_value_result = await variable.getValue(shell, t_shell_command, sc_event, presentArguments, 
            // Define a recursive callback that can be used to parse possible variables in a default value of the current variable.
            (raw_default_value) => {
                // Avoid circular references by removing the current variable from the set of parseable variables.
                // This will cumulate in deep nested parsing: Possible deeper parsing rounds will always have narrower
                // and narrower sets of variables to parse.
                const reduced_variables = removeFromSet(variables, variable);
                return parseVariables(plugin, raw_default_value, shell, false, // Disable escaping special characters at this phase to avoid double escaping, as escaping will be done later.
                t_shell_command, sc_event, reduced_variables, raw_value_augmenter, escaped_value_augmenter);
            });
            // Allow custom modification of the raw value.
            if (raw_value_augmenter) {
                // The augmenter can modify the content of the variable_value_result object.
                raw_value_augmenter(variable, variable_value_result);
            }
            const raw_variable_value = variable_value_result.value;
            // Check possible error messages that might have come from rendering.
            if (variable_value_result.succeeded) {
                // Parsing was ok.
                // Escape the value if needed.
                let use_variable_value;
                if (escapeCurrentVariable) {
                    // Use an escaped value.
                    use_variable_value = shell.escapeValue(// shell is always a Shell object when escapeCurrentVariable is true.
                    raw_variable_value);
                }
                else {
                    // No escaping is wanted, so use the raw value.
                    use_variable_value = raw_variable_value; // raw_variable_value is always a string when variable_value_result.succeeded is true.
                }
                // Augment the escaped value, if wanted.
                if (escaped_value_augmenter) {
                    use_variable_value = escaped_value_augmenter(variable, use_variable_value, raw_variable_value);
                }
                // Replace the variable name with the variable value.
                parsing_result.parsed_content = parsing_result.parsed_content /* not null */.replace(substitute, () => {
                    // Do the replacing in a function in order to avoid a possible $ character to be interpreted by JavaScript to interact with the regex.
                    // More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter (referenced 2021-11-02.)
                    return use_variable_value;
                });
            }
            else {
                // There has been problem(s) with this variable.
                debugLog("parseVariables(): Parsing content " + content + " failed.");
                parsing_result.succeeded = false;
                parsing_result.parsed_content = null;
                parsing_result.error_messages = variable_value_result.error_messages; // Returning now prevents parsing rest of the variables.
                return parsing_result;
            }
        }
    }
    debugLog("parseVariables(): Parsing content succeeded: From '" + content + "' to '" + parsing_result.parsed_content + "'");
    parsing_result.succeeded = true;
    return parsing_result;
}
/**
 * Parses just a single Variable in content, and does it synchronously. An alternative to parseVariables() in situations
 * where asynchronous functions should be avoided, e.g. in Obsidian Command palette, or file/folder/editor menus (although
 * this is not used in those menus atm).
 *
 * @param content
 * @param variable Can only be a Variable that implements the method generateValueSynchronously(). Otherwise an Error is thrown. Also, does not support variables that have parameters, at least at the moment.
 * @param shell
 * @return A ParsingResult similar to what parseVariables() returns, but directly, not in a Promise.
 */
function parseVariableSynchronously(content, variable, shell) {
    if (variable.getParameterNames().length > 0) {
        throw new Error("parseVariableSynchronously() does not support variables with parameters at the moment. Variable: " + variable.constructor.name);
    }
    const parsingResult = {
        // Initial values, will be overridden.
        succeeded: false,
        original_content: content,
        parsed_content: null,
        error_messages: [],
        count_parsed_variables: 0,
    };
    // Get the Variable's value.
    const variableValueResult = variable.getValueSynchronously(); // Shell could be passed here as a parameter, if there will ever be a need to use this method to access any variables that need the Shell (e.g. file path related variables that do path translation). It's not done now, as there's no need for now.
    if (variableValueResult.succeeded) {
        // Parsing succeeded.
        parsingResult.succeeded = true;
        parsingResult.parsed_content = content.replaceAll(getVariableRegExp(variable), // Even thought this regexp actually supports arguments, supplying arguments to variables is not implemented in variable.getValueSynchronously(), so variables expecting parameters cannot be supported at the moment.
        (occurrence) => {
            parsingResult.count_parsed_variables++; // The count is not used (at least at the moment of writing this), but might be used in the future.
            // Check if special characters should be escaped or not.
            const escape = !doesOccurrenceDenyEscaping(occurrence);
            if (escape) {
                // Do escape.
                return shell.escapeValue(variableValueResult.value);
            }
            else {
                // No escaping.
                return variableValueResult.value; // Replace {{variable}} with a value.
            }
        });
    }
    else {
        // Parsing failed.
        parsingResult.error_messages = variableValueResult.error_messages;
    }
    return parsingResult;
}
/**
 * Reads all variables from the content string, and returns a VariableSet containing all the found variables.
 *
 * This is needed in situations where variables will not be parsed (= variable values are not needed), but where it's just
 * needed to know what variables e.g. a shell command relies on.
 *
 * @param plugin
 * @param contents Can be a single string, or an array of strings, if it's needed to look for variables in multiple contents (e.g. all platform versions of a shell command).
 * @param searchForVariables If not defined, will use all variables.
 */
function getUsedVariables(plugin, contents, searchForVariables = plugin.getVariables()) {
    if (searchForVariables instanceof Variable) {
        // searchForVariables is a single Variable.
        // Convert it to a VariableSet.
        searchForVariables = new VariableSet([searchForVariables]);
    }
    if (typeof contents === "string") {
        // contents is a single content. Convert it to an array.
        contents = [contents];
    }
    const found_variables = new VariableMap();
    for (const variable of searchForVariables) {
        const pattern = getVariableRegExp(variable);
        for (const content of contents) {
            if (pattern.exec(content) !== null) {
                // This variable was found.
                found_variables.set(variable.getIdentifier(), variable);
                break;
            }
        }
    }
    return found_variables;
}
function getVariableRegExp(variable) {
    return new RegExp(variable.getPattern(), "igu"); // i: case-insensitive; g: match all occurrences instead of just the first one. u: support 4-byte unicode characters too.
}
function doesOccurrenceDenyEscaping(occurrence) {
    return "{{!" === occurrence.slice(0, 3); // .slice(0, 3) = get characters 0...2, so stop before 3. The 'end' parameter is confusing.
}
/*
// TODO: Rewrite ParsingResult to the following format that better defines the 'succeeded' property's relationship to 'parsed_content' and 'error_messages'.
// Then find all "parsed_content as string" expressions in the whole project and remove the "as string" parts, they should be redundant after this change.

export type ParsingResult = ParsingResultSucceeded | ParsingResultFailed;

interface ParsingResultSucceeded extends ParsingResultBase {
    succeeded: true;
    parsed_content: string;
    error_messages: []; // An always empty array.
}

interface ParsingResultFailed extends ParsingResultBase {
    succeeded: false;
    parsed_content: null;
    error_messages: [string, ...string[]]; // A non-empty array of strings.
}

interface ParsingResultBase {
    original_content: string;
    count_parsed_variables: number;
}
 */

var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

function getDefaultExportFromCjs (x) {
	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}

function createCommonjsModule(fn) {
  var module = { exports: {} };
	return fn(module, module.exports), module.exports;
}

/*  ansi_up.js
 *  author : Dru Nelson
 *  license : MIT
 *  http://github.com/drudru/ansi_up
 */

var ansi_up = createCommonjsModule(function (module, exports) {
(function (root, factory) {
    if (typeof exports.nodeName !== 'string') {
        // CommonJS
        factory(exports);
    } else {
        // Browser globals
        var exp = {};
        factory(exp);
        root.AnsiUp = exp.default;
    }
}(commonjsGlobal, function (exports) {
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
    if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
    return cooked;
};
var PacketKind;
(function (PacketKind) {
    PacketKind[PacketKind["EOS"] = 0] = "EOS";
    PacketKind[PacketKind["Text"] = 1] = "Text";
    PacketKind[PacketKind["Incomplete"] = 2] = "Incomplete";
    PacketKind[PacketKind["ESC"] = 3] = "ESC";
    PacketKind[PacketKind["Unknown"] = 4] = "Unknown";
    PacketKind[PacketKind["SGR"] = 5] = "SGR";
    PacketKind[PacketKind["OSCURL"] = 6] = "OSCURL";
})(PacketKind || (PacketKind = {}));
var AnsiUp = (function () {
    function AnsiUp() {
        this.VERSION = "5.1.0";
        this.setup_palettes();
        this._use_classes = false;
        this.bold = false;
        this.italic = false;
        this.underline = false;
        this.fg = this.bg = null;
        this._buffer = '';
        this._url_whitelist = { 'http': 1, 'https': 1 };
        this._escape_html = true;
    }
    Object.defineProperty(AnsiUp.prototype, "use_classes", {
        get: function () {
            return this._use_classes;
        },
        set: function (arg) {
            this._use_classes = arg;
        },
        enumerable: false,
        configurable: true
    });
    Object.defineProperty(AnsiUp.prototype, "url_whitelist", {
        get: function () {
            return this._url_whitelist;
        },
        set: function (arg) {
            this._url_whitelist = arg;
        },
        enumerable: false,
        configurable: true
    });
    Object.defineProperty(AnsiUp.prototype, "escape_html", {
        get: function () {
            return this._escape_html;
        },
        set: function (arg) {
            this._escape_html = arg;
        },
        enumerable: false,
        configurable: true
    });
    AnsiUp.prototype.setup_palettes = function () {
        var _this = this;
        this.ansi_colors =
            [
                [
                    { rgb: [0, 0, 0], class_name: "ansi-black" },
                    { rgb: [187, 0, 0], class_name: "ansi-red" },
                    { rgb: [0, 187, 0], class_name: "ansi-green" },
                    { rgb: [187, 187, 0], class_name: "ansi-yellow" },
                    { rgb: [0, 0, 187], class_name: "ansi-blue" },
                    { rgb: [187, 0, 187], class_name: "ansi-magenta" },
                    { rgb: [0, 187, 187], class_name: "ansi-cyan" },
                    { rgb: [255, 255, 255], class_name: "ansi-white" }
                ],
                [
                    { rgb: [85, 85, 85], class_name: "ansi-bright-black" },
                    { rgb: [255, 85, 85], class_name: "ansi-bright-red" },
                    { rgb: [0, 255, 0], class_name: "ansi-bright-green" },
                    { rgb: [255, 255, 85], class_name: "ansi-bright-yellow" },
                    { rgb: [85, 85, 255], class_name: "ansi-bright-blue" },
                    { rgb: [255, 85, 255], class_name: "ansi-bright-magenta" },
                    { rgb: [85, 255, 255], class_name: "ansi-bright-cyan" },
                    { rgb: [255, 255, 255], class_name: "ansi-bright-white" }
                ]
            ];
        this.palette_256 = [];
        this.ansi_colors.forEach(function (palette) {
            palette.forEach(function (rec) {
                _this.palette_256.push(rec);
            });
        });
        var levels = [0, 95, 135, 175, 215, 255];
        for (var r = 0; r < 6; ++r) {
            for (var g = 0; g < 6; ++g) {
                for (var b = 0; b < 6; ++b) {
                    var col = { rgb: [levels[r], levels[g], levels[b]], class_name: 'truecolor' };
                    this.palette_256.push(col);
                }
            }
        }
        var grey_level = 8;
        for (var i = 0; i < 24; ++i, grey_level += 10) {
            var gry = { rgb: [grey_level, grey_level, grey_level], class_name: 'truecolor' };
            this.palette_256.push(gry);
        }
    };
    AnsiUp.prototype.escape_txt_for_html = function (txt) {
        if (!this._escape_html)
            return txt;
        return txt.replace(/[&<>"']/gm, function (str) {
            if (str === "&")
                return "&amp;";
            if (str === "<")
                return "&lt;";
            if (str === ">")
                return "&gt;";
            if (str === "\"")
                return "&quot;";
            if (str === "'")
                return "&#x27;";
        });
    };
    AnsiUp.prototype.append_buffer = function (txt) {
        var str = this._buffer + txt;
        this._buffer = str;
    };
    AnsiUp.prototype.get_next_packet = function () {
        var pkt = {
            kind: PacketKind.EOS,
            text: '',
            url: ''
        };
        var len = this._buffer.length;
        if (len == 0)
            return pkt;
        var pos = this._buffer.indexOf("\x1B");
        if (pos == -1) {
            pkt.kind = PacketKind.Text;
            pkt.text = this._buffer;
            this._buffer = '';
            return pkt;
        }
        if (pos > 0) {
            pkt.kind = PacketKind.Text;
            pkt.text = this._buffer.slice(0, pos);
            this._buffer = this._buffer.slice(pos);
            return pkt;
        }
        if (pos == 0) {
            if (len < 3) {
                pkt.kind = PacketKind.Incomplete;
                return pkt;
            }
            var next_char = this._buffer.charAt(1);
            if ((next_char != '[') && (next_char != ']') && (next_char != '(')) {
                pkt.kind = PacketKind.ESC;
                pkt.text = this._buffer.slice(0, 1);
                this._buffer = this._buffer.slice(1);
                return pkt;
            }
            if (next_char == '[') {
                if (!this._csi_regex) {
                    this._csi_regex = rgx(__makeTemplateObject(["\n                        ^                           # beginning of line\n                                                    #\n                                                    # First attempt\n                        (?:                         # legal sequence\n                          \u001B[                      # CSI\n                          ([<-?]?)              # private-mode char\n                          ([d;]*)                    # any digits or semicolons\n                          ([ -/]?               # an intermediate modifier\n                          [@-~])                # the command\n                        )\n                        |                           # alternate (second attempt)\n                        (?:                         # illegal sequence\n                          \u001B[                      # CSI\n                          [ -~]*                # anything legal\n                          ([\0-\u001F:])              # anything illegal\n                        )\n                    "], ["\n                        ^                           # beginning of line\n                                                    #\n                                                    # First attempt\n                        (?:                         # legal sequence\n                          \\x1b\\[                      # CSI\n                          ([\\x3c-\\x3f]?)              # private-mode char\n                          ([\\d;]*)                    # any digits or semicolons\n                          ([\\x20-\\x2f]?               # an intermediate modifier\n                          [\\x40-\\x7e])                # the command\n                        )\n                        |                           # alternate (second attempt)\n                        (?:                         # illegal sequence\n                          \\x1b\\[                      # CSI\n                          [\\x20-\\x7e]*                # anything legal\n                          ([\\x00-\\x1f:])              # anything illegal\n                        )\n                    "]));
                }
                var match = this._buffer.match(this._csi_regex);
                if (match === null) {
                    pkt.kind = PacketKind.Incomplete;
                    return pkt;
                }
                if (match[4]) {
                    pkt.kind = PacketKind.ESC;
                    pkt.text = this._buffer.slice(0, 1);
                    this._buffer = this._buffer.slice(1);
                    return pkt;
                }
                if ((match[1] != '') || (match[3] != 'm'))
                    pkt.kind = PacketKind.Unknown;
                else
                    pkt.kind = PacketKind.SGR;
                pkt.text = match[2];
                var rpos = match[0].length;
                this._buffer = this._buffer.slice(rpos);
                return pkt;
            }
            else if (next_char == ']') {
                if (len < 4) {
                    pkt.kind = PacketKind.Incomplete;
                    return pkt;
                }
                if ((this._buffer.charAt(2) != '8')
                    || (this._buffer.charAt(3) != ';')) {
                    pkt.kind = PacketKind.ESC;
                    pkt.text = this._buffer.slice(0, 1);
                    this._buffer = this._buffer.slice(1);
                    return pkt;
                }
                if (!this._osc_st) {
                    this._osc_st = rgxG(__makeTemplateObject(["\n                        (?:                         # legal sequence\n                          (\u001B\\)                    # ESC                           |                           # alternate\n                          (\u0007)                      # BEL (what xterm did)\n                        )\n                        |                           # alternate (second attempt)\n                        (                           # illegal sequence\n                          [\0-\u0006]                 # anything illegal\n                          |                           # alternate\n                          [\b-\u001A]                 # anything illegal\n                          |                           # alternate\n                          [\u001C-\u001F]                 # anything illegal\n                        )\n                    "], ["\n                        (?:                         # legal sequence\n                          (\\x1b\\\\)                    # ESC \\\n                          |                           # alternate\n                          (\\x07)                      # BEL (what xterm did)\n                        )\n                        |                           # alternate (second attempt)\n                        (                           # illegal sequence\n                          [\\x00-\\x06]                 # anything illegal\n                          |                           # alternate\n                          [\\x08-\\x1a]                 # anything illegal\n                          |                           # alternate\n                          [\\x1c-\\x1f]                 # anything illegal\n                        )\n                    "]));
                }
                this._osc_st.lastIndex = 0;
                {
                    var match_1 = this._osc_st.exec(this._buffer);
                    if (match_1 === null) {
                        pkt.kind = PacketKind.Incomplete;
                        return pkt;
                    }
                    if (match_1[3]) {
                        pkt.kind = PacketKind.ESC;
                        pkt.text = this._buffer.slice(0, 1);
                        this._buffer = this._buffer.slice(1);
                        return pkt;
                    }
                }
                {
                    var match_2 = this._osc_st.exec(this._buffer);
                    if (match_2 === null) {
                        pkt.kind = PacketKind.Incomplete;
                        return pkt;
                    }
                    if (match_2[3]) {
                        pkt.kind = PacketKind.ESC;
                        pkt.text = this._buffer.slice(0, 1);
                        this._buffer = this._buffer.slice(1);
                        return pkt;
                    }
                }
                if (!this._osc_regex) {
                    this._osc_regex = rgx(__makeTemplateObject(["\n                        ^                           # beginning of line\n                                                    #\n                        \u001B]8;                    # OSC Hyperlink\n                        [ -:<-~]*       # params (excluding ;)\n                        ;                           # end of params\n                        ([!-~]{0,512})        # URL capture\n                        (?:                         # ST\n                          (?:\u001B\\)                  # ESC                           |                           # alternate\n                          (?:\u0007)                    # BEL (what xterm did)\n                        )\n                        ([ -~]+)              # TEXT capture\n                        \u001B]8;;                   # OSC Hyperlink End\n                        (?:                         # ST\n                          (?:\u001B\\)                  # ESC                           |                           # alternate\n                          (?:\u0007)                    # BEL (what xterm did)\n                        )\n                    "], ["\n                        ^                           # beginning of line\n                                                    #\n                        \\x1b\\]8;                    # OSC Hyperlink\n                        [\\x20-\\x3a\\x3c-\\x7e]*       # params (excluding ;)\n                        ;                           # end of params\n                        ([\\x21-\\x7e]{0,512})        # URL capture\n                        (?:                         # ST\n                          (?:\\x1b\\\\)                  # ESC \\\n                          |                           # alternate\n                          (?:\\x07)                    # BEL (what xterm did)\n                        )\n                        ([\\x20-\\x7e]+)              # TEXT capture\n                        \\x1b\\]8;;                   # OSC Hyperlink End\n                        (?:                         # ST\n                          (?:\\x1b\\\\)                  # ESC \\\n                          |                           # alternate\n                          (?:\\x07)                    # BEL (what xterm did)\n                        )\n                    "]));
                }
                var match = this._buffer.match(this._osc_regex);
                if (match === null) {
                    pkt.kind = PacketKind.ESC;
                    pkt.text = this._buffer.slice(0, 1);
                    this._buffer = this._buffer.slice(1);
                    return pkt;
                }
                pkt.kind = PacketKind.OSCURL;
                pkt.url = match[1];
                pkt.text = match[2];
                var rpos = match[0].length;
                this._buffer = this._buffer.slice(rpos);
                return pkt;
            }
            else if (next_char == '(') {
                pkt.kind = PacketKind.Unknown;
                this._buffer = this._buffer.slice(3);
                return pkt;
            }
        }
    };
    AnsiUp.prototype.ansi_to_html = function (txt) {
        this.append_buffer(txt);
        var blocks = [];
        while (true) {
            var packet = this.get_next_packet();
            if ((packet.kind == PacketKind.EOS)
                || (packet.kind == PacketKind.Incomplete))
                break;
            if ((packet.kind == PacketKind.ESC)
                || (packet.kind == PacketKind.Unknown))
                continue;
            if (packet.kind == PacketKind.Text)
                blocks.push(this.transform_to_html(this.with_state(packet)));
            else if (packet.kind == PacketKind.SGR)
                this.process_ansi(packet);
            else if (packet.kind == PacketKind.OSCURL)
                blocks.push(this.process_hyperlink(packet));
        }
        return blocks.join("");
    };
    AnsiUp.prototype.with_state = function (pkt) {
        return { bold: this.bold, italic: this.italic, underline: this.underline, fg: this.fg, bg: this.bg, text: pkt.text };
    };
    AnsiUp.prototype.process_ansi = function (pkt) {
        var sgr_cmds = pkt.text.split(';');
        while (sgr_cmds.length > 0) {
            var sgr_cmd_str = sgr_cmds.shift();
            var num = parseInt(sgr_cmd_str, 10);
            if (isNaN(num) || num === 0) {
                this.fg = this.bg = null;
                this.bold = false;
                this.italic = false;
                this.underline = false;
            }
            else if (num === 1) {
                this.bold = true;
            }
            else if (num === 3) {
                this.italic = true;
            }
            else if (num === 4) {
                this.underline = true;
            }
            else if (num === 22) {
                this.bold = false;
            }
            else if (num === 23) {
                this.italic = false;
            }
            else if (num === 24) {
                this.underline = false;
            }
            else if (num === 39) {
                this.fg = null;
            }
            else if (num === 49) {
                this.bg = null;
            }
            else if ((num >= 30) && (num < 38)) {
                this.fg = this.ansi_colors[0][(num - 30)];
            }
            else if ((num >= 40) && (num < 48)) {
                this.bg = this.ansi_colors[0][(num - 40)];
            }
            else if ((num >= 90) && (num < 98)) {
                this.fg = this.ansi_colors[1][(num - 90)];
            }
            else if ((num >= 100) && (num < 108)) {
                this.bg = this.ansi_colors[1][(num - 100)];
            }
            else if (num === 38 || num === 48) {
                if (sgr_cmds.length > 0) {
                    var is_foreground = (num === 38);
                    var mode_cmd = sgr_cmds.shift();
                    if (mode_cmd === '5' && sgr_cmds.length > 0) {
                        var palette_index = parseInt(sgr_cmds.shift(), 10);
                        if (palette_index >= 0 && palette_index <= 255) {
                            if (is_foreground)
                                this.fg = this.palette_256[palette_index];
                            else
                                this.bg = this.palette_256[palette_index];
                        }
                    }
                    if (mode_cmd === '2' && sgr_cmds.length > 2) {
                        var r = parseInt(sgr_cmds.shift(), 10);
                        var g = parseInt(sgr_cmds.shift(), 10);
                        var b = parseInt(sgr_cmds.shift(), 10);
                        if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
                            var c = { rgb: [r, g, b], class_name: 'truecolor' };
                            if (is_foreground)
                                this.fg = c;
                            else
                                this.bg = c;
                        }
                    }
                }
            }
        }
    };
    AnsiUp.prototype.transform_to_html = function (fragment) {
        var txt = fragment.text;
        if (txt.length === 0)
            return txt;
        txt = this.escape_txt_for_html(txt);
        if (!fragment.bold && !fragment.italic && !fragment.underline && fragment.fg === null && fragment.bg === null)
            return txt;
        var styles = [];
        var classes = [];
        var fg = fragment.fg;
        var bg = fragment.bg;
        if (fragment.bold)
            styles.push('font-weight:bold');
        if (fragment.italic)
            styles.push('font-style:italic');
        if (fragment.underline)
            styles.push('text-decoration:underline');
        if (!this._use_classes) {
            if (fg)
                styles.push("color:rgb(" + fg.rgb.join(',') + ")");
            if (bg)
                styles.push("background-color:rgb(" + bg.rgb + ")");
        }
        else {
            if (fg) {
                if (fg.class_name !== 'truecolor') {
                    classes.push(fg.class_name + "-fg");
                }
                else {
                    styles.push("color:rgb(" + fg.rgb.join(',') + ")");
                }
            }
            if (bg) {
                if (bg.class_name !== 'truecolor') {
                    classes.push(bg.class_name + "-bg");
                }
                else {
                    styles.push("background-color:rgb(" + bg.rgb.join(',') + ")");
                }
            }
        }
        var class_string = '';
        var style_string = '';
        if (classes.length)
            class_string = " class=\"" + classes.join(' ') + "\"";
        if (styles.length)
            style_string = " style=\"" + styles.join(';') + "\"";
        return "<span" + style_string + class_string + ">" + txt + "</span>";
    };
    AnsiUp.prototype.process_hyperlink = function (pkt) {
        var parts = pkt.url.split(':');
        if (parts.length < 1)
            return '';
        if (!this._url_whitelist[parts[0]])
            return '';
        var result = "<a href=\"" + this.escape_txt_for_html(pkt.url) + "\">" + this.escape_txt_for_html(pkt.text) + "</a>";
        return result;
    };
    return AnsiUp;
}());
function rgx(tmplObj) {
    var subst = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        subst[_i - 1] = arguments[_i];
    }
    var regexText = tmplObj.raw[0];
    var wsrgx = /^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm;
    var txt2 = regexText.replace(wsrgx, '');
    return new RegExp(txt2);
}
function rgxG(tmplObj) {
    var subst = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        subst[_i - 1] = arguments[_i];
    }
    var regexText = tmplObj.raw[0];
    var wsrgx = /^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm;
    var txt2 = regexText.replace(wsrgx, '');
    return new RegExp(txt2, 'g');
}

    Object.defineProperty(exports, "__esModule", { value: true });
    exports.default = AnsiUp;
}));
});

var AnsiUp = /*@__PURE__*/getDefaultExportFromCjs(ansi_up);

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * TODO: Rename to OutputHandler.
 */
class OutputChannel {
    /**
     * @param plugin
     * @param t_shell_command
     * @param shell_command_parsing_result
     * @param outputHandlingMode
     * @param processTerminator Will be called if user decides to end the process. Set to null if the process has already ended.
     */
    constructor(plugin, t_shell_command, shell_command_parsing_result, outputHandlingMode, processTerminator) {
        this.plugin = plugin;
        this.t_shell_command = t_shell_command;
        this.shell_command_parsing_result = shell_command_parsing_result;
        this.outputHandlingMode = outputHandlingMode;
        this.processTerminator = processTerminator;
        this.app = plugin.app;
        this.initializeAnsiToHtmlConverter();
        this.initialize();
    }
    /**
     * Can be overridden in child classes in order to vary the title depending on output_stream.
     * @param output_stream
     */
    static getTitle(output_stream) {
        return this.title;
    }
    /**
     * Sub classes can do here initializations that are common to both handleBuffered() and handleRealtime().
     *
     * Inits could be done in contructor(), too, but this is cleaner - no need to deal with parameters and no need for a super()
     * call.
     *
     * @protected
     */
    initialize() {
        // Do nothing by default.
    }
    async handleBuffered(output, error_code, enableOutputWrapping = true) {
        this.requireHandlingMode("buffered");
        // Qualify output
        if (OutputChannel.isOutputEmpty(output)) {
            // The output is empty
            if (!this.static().accepts_empty_output) {
                // This OutputChannel does not accept empty output, i.e. empty output should be just ignored.
                debugLog(this.constructor.name + ".handleBuffered(): Ignoring empty output.");
                return;
            }
        }
        debugLog(this.constructor.name + ".handleBuffered(): Handling output...");
        // Output is ok.
        // Apply ANSI conversion, if enabled.
        let outputStreamName;
        for (outputStreamName in output) {
            output[outputStreamName] = this.convertAnsiCodeToHtmlIfEnabled(output[outputStreamName], outputStreamName);
        }
        // Handle output.
        await this._handleBuffered(await this.prepare_output(output, enableOutputWrapping), error_code);
        debugLog("Output handling is done.");
    }
    /**
     * @param outputStreamName
     * @param outputContent
     * @param enableOutputWrapping No caller actually sets this to false at the moment, unlike the handleBuffered() method's counterpart. But have this just in case.
     */
    async handleRealtime(outputStreamName, outputContent, enableOutputWrapping = true) {
        this.requireHandlingMode("realtime");
        // Qualify output
        if ("" === outputContent) {
            // The output is empty
            if (!this.static().accepts_empty_output) {
                // This OutputChannel does not accept empty output, i.e. empty output should be just ignored.
                debugLog(this.constructor.name + ".handleRealtime(): Ignoring empty output.");
                return;
            }
        }
        debugLog(this.constructor.name + ".handleRealtime(): Handling output...");
        // Output is ok.
        // If allowed, wrap the output with output wrapper text.
        if (enableOutputWrapping) {
            // Wrap output (but only if a wrapper is defined)
            outputContent = await this.wrapOutput(outputStreamName, outputContent);
        }
        // Apply ANSI conversion, if enabled.
        outputContent = this.convertAnsiCodeToHtmlIfEnabled(outputContent, outputStreamName);
        // Handle output.
        await this._handleRealtime(outputContent, outputStreamName);
        debugLog("Output handling is done.");
    }
    _endRealtime(exitCode) {
        // Do nothing by default.
    }
    /**
     * When a shell command is executed in "realtime" mode, a separate ending call should be made in order to pass an
     * exit code to the OutputChannel. Some OutputChannels display the code to user, but most do not.
     *
     * @param exitCode
     */
    endRealtime(exitCode) {
        this.requireHandlingMode("realtime");
        this._endRealtime(exitCode);
    }
    requireHandlingMode(requiredMode) {
        if (this.outputHandlingMode !== requiredMode) {
            throw new Error("this.outputHandlingMode must be '" + requiredMode + "'.");
        }
    }
    static acceptsOutputStream(output_stream) {
        return this.accepted_output_streams.contains(output_stream);
    }
    /**
     * Does the following preparations:
     *  - Combines output streams (if wanted by the OutputChannel).
     *  - Wraps output (if defined in shell command configuration).
     * @param output_streams
     * @param enableOutputWrapping
     * @private
     */
    async prepare_output(output_streams, enableOutputWrapping) {
        const wrapOutputIfEnabled = async (outputStreamName, outputContent) => {
            if (enableOutputWrapping) {
                // Wrap output content.
                return await this.wrapOutput(outputStreamName, outputContent);
            }
            else {
                // Wrapping is disabled, return unmodified output content.
                return outputContent;
            }
        };
        const wrap_outputs_separately = async () => {
            const wrapped_output_streams = {};
            let output_stream_name;
            for (output_stream_name in output_streams) {
                wrapped_output_streams[output_stream_name] = await wrapOutputIfEnabled(output_stream_name, output_streams[output_stream_name]);
            }
            return wrapped_output_streams;
        };
        // Check if outputs should be combined.
        const combineOutputStreams = this.static().combine_output_streams; // Can be an empty string "", and outputs should still be joined.
        if (typeof combineOutputStreams === "string") {
            // Combine output strings into a single string.
            // Can output wrapping be combined?
            if (this.t_shell_command.isOutputWrapperStdoutSameAsStderr()) {
                // Output wrapping can be combined.
                return await wrapOutputIfEnabled("stdout", joinObjectProperties(output_streams, combineOutputStreams));
            }
            else {
                // Output wrapping needs to be done separately.
                const wrapped_output_streams = await wrap_outputs_separately();
                return joinObjectProperties(wrapped_output_streams, combineOutputStreams); // Use combineOutputStreams as a glue string.
            }
        }
        else {
            // Do not combine, handle each stream separately
            return await wrap_outputs_separately();
        }
    }
    /**
     * Surrounds the given output text with an output wrapper. If no output wrapper is defined, returns the original
     * output text without any modifications.
     */
    async wrapOutput(output_stream, output_content) {
        // Get preparsed output wrapper content. It has all other variables parsed, except {{output}}.
        const parsing_result_key = "output_wrapper_" + output_stream;
        const output_wrapper_content = this.shell_command_parsing_result[parsing_result_key];
        // Check if output wrapper content exists.
        if (undefined === output_wrapper_content) {
            // No OutputWrapper is defined for this shell command.
            // Return the output text without modifications.
            debugLog("Output wrapping: No wrapper is defined for '" + output_stream + "'.");
            return output_content;
        }
        // Parse the {{output}} variable
        const output_variable = new Variable_Output(this.plugin, output_content);
        const parsing_result = await parseVariables(this.plugin, output_wrapper_content, this.t_shell_command.getShell(), // Even though the shell won't get executed anymore, possible file path related variables need it for directory path formatting.
        false, // No shell is executed anymore, so no need for escaping.
        this.t_shell_command, null, // No support for {{event_*}} variables is needed, because they are already parsed in output_wrapper_content. This phase only parses {{output}} variable, nothing else.
        new VariableSet([output_variable]));
        // Inspect the parsing result. It should always succeed, as the {{output}} variable should not give any errors.
        if (parsing_result.succeeded) {
            // Succeeded.
            debugLog("Output wrapping: Wrapping " + output_stream + " succeeded.");
            return parsing_result.parsed_content;
        }
        else {
            // Failed for some reason.
            this.plugin.newError("Output wrapping failed, see error(s) below.");
            this.plugin.newErrors(parsing_result.error_messages);
            throw new Error("Output wrapping failed: Parsing {{output}} resulted in error(s): " + parsing_result.error_messages.join(" "));
        }
    }
    /**
     * Can be moved to a global function isOutputStreamEmpty() if needed.
     * @param output
     * @private
     */
    static isOutputEmpty(output) {
        if (undefined !== output.stderr) {
            return false;
        }
        return undefined === output.stdout || "" === output.stdout;
    }
    /**
     * Output can contain font styles, colors and links in ANSI code format. This method defines a converter for ANSI code.
     *
     * @see https://en.wikipedia.org/wiki/ANSI_escape_code
     * @private
     */
    initializeAnsiToHtmlConverter() {
        this.ansiToHtmlConverter = new AnsiUp;
        // this.ansiToHtmlConverter.use_classes = true; // Use CSS classes instead of style="" attributes. Commented out, because it doesn't substitute all style="" attributes (e.g. true-colors are still defined using style="", and so al bolds, italics etc.). TODO: If wanted, can later make a pull request to the AnsiUp library that would substitute all style="" attributes with classes (except true-colors).
        this.ansiToHtmlConverter.escape_html = false; // Do not escape possibly outputted HTML. // TODO: Create a setting for this. Escaping html in the output might be handy. Or maybe it should escape also Markdown special characters (so would be done elsewhere)?
        Object.assign(this.ansiToHtmlConverter.url_whitelist, {
            "obsidian": 1, // Whitelist obsidian:// protocol in possible links. This is needed if the converted ANSI code contains hyperlinks.
            // https:// and http:// are already whitelisted.
        });
    }
    /**
     * Two thing can deny the ANSI conversion:
     *  1) OutputHandlerConfiguration
     *  2) An OutputChannel subclass. At least "Open files" denies it.
     * @param outputContent
     * @param outputStreamName
     * @protected
     */
    convertAnsiCodeToHtmlIfEnabled(outputContent, outputStreamName) {
        if (!this.static().applicableConfiguration.convert_ansi_code) {
            // A subclass has disabled the conversion.
            return outputContent;
        }
        const outputHandlerConfigurations = this.t_shell_command.getOutputHandlers();
        if (outputHandlerConfigurations[outputStreamName].convert_ansi_code) {
            // Converting is allowed.
            return this.ansiToHtmlConverter.ansi_to_html(outputContent);
        }
        else {
            // user configuration has disabled the conversion.
            return outputContent;
        }
    }
    static() {
        return this.constructor;
    }
    static getDefaultConfiguration(outputChannelCode) {
        return {
            handler: outputChannelCode,
            convert_ansi_code: true,
        };
    }
    newError(errorMessage) {
        this.plugin.newError(this.static().title + ": " + errorMessage);
    }
}
OutputChannel.accepted_output_streams = ["stdout", "stderr"];
OutputChannel.accepts_empty_output = false;
/**
 * Determines if the output channel wants to handle a unified output or not. If yes, this property should define a
 * delimiter string that will be used as a glue between different output streams.
 *
 * @protected
 */
OutputChannel.combine_output_streams = false;
/**
 * Used in OutputModal to redirect output based on hotkeys. If this is undefined, then the output channel is completely
 * excluded from OutputModal.
 */
OutputChannel.hotkey_letter = undefined;
OutputChannel.applicableConfiguration = {
    /**
     * Whether to allow convertAnsiCodeToHtmlIfAllowed() to do conversion. Note that even if this is true, user
     * configuration may disable it.
     *
     * @see convertAnsiCodeToHtmlIfEnabled
     * @private
     */
    convert_ansi_code: true,
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_Notification extends OutputChannel {
    constructor() {
        super(...arguments);
        /**
         * All received output cumulatively. Subsequent handlings will then use the whole output, not just new parts.
         * Only used in "realtime" mode.
         *
         * @private
         */
        this.realtimeContentBuffer = "";
        /**
         * A flag for indicating that if any stderr output has happened, all subsequent handlings should format the output
         * Notice message with error formatting (i.e. show [...] at the beginning of the message).
         * @private
         */
        this.realtimeHasStderrOccurred = false;
    }
    static getTitle(output_stream) {
        switch (output_stream) {
            case "stdout":
                return "Notification balloon";
            case "stderr":
                return "Error balloon";
        }
    }
    async _handleBuffered(output, error_code) {
        // Iterate output streams.
        // There can be both "stdout" and "stderr" present at the same time, or just one of them. If both are present, two
        // notifications will be created.
        let output_stream_name;
        for (output_stream_name in output) {
            const output_message = output[output_stream_name]; // as string = output message is not undefined because of the for loop.
            this.notify(output_stream_name, output_message, error_code);
        }
    }
    async _handleRealtime(outputContent, outputStreamName) {
        // Append new content
        this.realtimeContentBuffer += outputContent;
        // Raise a flag if seeing 'stderr' output.
        if ("stderr" === outputStreamName) {
            this.realtimeHasStderrOccurred = true;
        }
        // Does a Notice exist already?
        if (this.realtimeNotice) {
            // Reuse an existing Notice.
            // Should output be formatted as an error message?
            let updatedMessage;
            if (this.realtimeHasStderrOccurred) {
                // Apply error formatting to output
                updatedMessage = OutputChannel_Notification.formatErrorMessage(this.realtimeContentBuffer, null);
            }
            else {
                // Use output as-is
                updatedMessage = this.realtimeContentBuffer;
            }
            // Use the updated output
            this.realtimeNotice.setMessage(this.prepareHTML(this.realtimeHasStderrOccurred ? "stderr" : "stdout", updatedMessage));
            // Update notice hiding timeout
            window.clearTimeout(this.realtimeNoticeTimeout); // Remove old timeout
            this.handleNotificationHiding(outputStreamName); // Add new timeout
        }
        else {
            // Create a new Notice.
            this.realtimeNotice = this.notify(this.realtimeHasStderrOccurred ? "stderr" : "stdout", this.realtimeContentBuffer, null, 0);
            // Create a timeout for hiding the Notice
            this.handleNotificationHiding(outputStreamName);
        }
        // Terminating button
        // @ts-ignore Notice.noticeEl belongs to Obsidian's PRIVATE API, and it may change without a prior notice. Only
        // create the button if noticeEl exists and is an HTMLElement.
        const noticeEl = this.realtimeNotice.noticeEl;
        if (null === this.processTerminator) {
            throw new Error("Process terminator is not set, although it should be set when handling output in realtime mode.");
        }
        if (undefined !== noticeEl && noticeEl instanceof HTMLElement) {
            this.plugin.createRequestTerminatingButton(noticeEl, this.processTerminator);
        }
    }
    _endRealtime(exitCode) {
        if (exitCode !== 0 || this.realtimeHasStderrOccurred) {
            // If a Notice exists, update it with the exitCode
            this.realtimeNotice?.setMessage(this.prepareHTML(this.realtimeHasStderrOccurred ? "stderr" : "stdout", OutputChannel_Notification.formatErrorMessage(this.realtimeContentBuffer, exitCode)));
        }
        // Remove terminating button
        // @ts-ignore Notice.noticeEl belongs to Obsidian's PRIVATE API, and it may change without a prior notice. Only
        // create the button if noticeEl exists and is an HTMLElement.
        const noticeEl = this.realtimeNotice?.noticeEl;
        if (undefined !== noticeEl && noticeEl instanceof HTMLElement) {
            noticeEl.find(".SC-icon-terminate-process")?.remove(); // ? = Only try to remove if the button exists. It does not exist if .setMessage() was called above as it overwrites all content in the Notice.
        }
    }
    /**
     *
     * @param outputStreamName
     * @param outputContent
     * @param exitCode
     * @param noticeTimeout Allows overriding the notice/error timeout setting.
     * @private
     */
    notify(outputStreamName, outputContent, exitCode, noticeTimeout) {
        switch (outputStreamName) {
            case "stdout":
                // Normal output
                return this.plugin.newNotification(this.prepareHTML(outputStreamName, outputContent), noticeTimeout ?? undefined);
            case "stderr":
                // Error output
                return this.plugin.newError(this.prepareHTML(outputStreamName, OutputChannel_Notification.formatErrorMessage(outputContent, exitCode)), noticeTimeout ?? undefined);
        }
    }
    static formatErrorMessage(outputContent, exitCode) {
        if (null === exitCode) {
            // If a "realtime" process is not finished, there is no exit code yet.
            // @ts-ignore Yea I know "..." is not a number nor null. :)
            exitCode = "...";
        }
        return "[" + exitCode + "]: " + outputContent;
    }
    handleNotificationHiding(outputStreamName) {
        // Hide by timeout
        let normalTimeout;
        switch (outputStreamName) {
            case "stdout":
                normalTimeout = this.plugin.getNotificationMessageDurationMs();
                break;
            case "stderr":
                normalTimeout = this.plugin.getErrorMessageDurationMs();
                break;
        }
        this.realtimeNoticeTimeout = window.setTimeout(() => {
            // Hide the Notice
            this.realtimeNotice?.hide(); // ? = Don't try to hide if a user has closed the notification by clicking. See the 'this.realtimeNotice = undefined;' line in the below click handler.
            this.realtimeNotice = undefined;
            this.realtimeNoticeTimeout = undefined;
        }, normalTimeout);
        // Subscribe to Notice's click event.
        // @ts-ignore Notice.noticeEl belongs to Obsidian's PRIVATE API, and it may change without a prior notice. Only
        // define the click listener if noticeEl exists and is an HTMLElement.
        const noticeEl = this.realtimeNotice.noticeEl;
        if (undefined !== noticeEl && noticeEl instanceof HTMLElement) {
            noticeEl.onClickEvent(() => {
                window.clearTimeout(this.realtimeNoticeTimeout); // Make sure timeout will not accidentally try to later hide an already hidden Notification.
                this.realtimeNoticeTimeout = undefined;
                this.realtimeNotice = undefined; // Give a signal to _handleRealtime() that if new output comes, a new Notice should be created.
            });
        }
    }
    /**
     * Wraps the given string content in a `<code></code>` element and creates a DocumentFragment for it.
     * @param outputStreamName Used to determine whether output can be wrapped in <code></code>.
     * @param outputContent
     * @private
     */
    prepareHTML(outputStreamName, outputContent) {
        // Can output be wrapped in <code></code> block?
        const decorationOption = this.plugin.settings.output_channel_notification_decorates_output;
        let canDecorate;
        switch (decorationOption) {
            case "stderr":
                // Can only wrap stderr output.
                canDecorate = outputStreamName === "stderr";
                break;
            default:
                // decorationOption is true or false.
                canDecorate = decorationOption;
                break;
        }
        return obsidian.sanitizeHTMLToDom(canDecorate
            ? "<code class=\"SC-output-channel-notification-monospace\">" + outputContent + "</code>" // Use <code> instead of <pre> to allow line wrapping.
            : outputContent);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_CurrentFile extends OutputChannel {
    async _handleBuffered(outputContent) {
        this.handle(outputContent);
    }
    async _handleRealtime(outputContent) {
        this.handle(outputContent);
    }
    handle(output_message) {
        const editor = getEditor(this.app);
        const view = getView(this.app);
        if (null === editor) {
            // For some reason it's not possible to get an editor.
            this.plugin.newError("Could not get an editor instance! Please create a discussion in GitHub. The command output is in the next error box:");
            this.plugin.newError(output_message); // Good to output it at least some way.
            debugLog("OutputChannel_CurrentFile: Could not get an editor instance.");
            return;
        }
        // Check if the view is in source mode
        if (null === view) {
            // For some reason it's not possible to get an editor, but it's not a big problem.
            debugLog("OutputChannel_CurrentFile: Could not get a view instance.");
        }
        else {
            // We do have a view
            if ("source" !== view.getMode()) {
                // Warn that the output might go to an unexpected place in the note file.
                this.plugin.newNotification("Note that your active note is not in 'Edit' mode! The output comes visible when you switch to 'Edit' mode again!");
            }
        }
        // Insert into the current file
        this.insertIntoEditor(editor, output_message);
    }
}
/**
 * There can be both "stdout" and "stderr" present at the same time, or just one of them. If both are present, they
 * will be joined together with " " as a separator.
 * @protected
 */
OutputChannel_CurrentFile.combine_output_streams = " ";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_CurrentFileCaret extends OutputChannel_CurrentFile {
    /**
     * Inserts text into the given editor, at caret position.
     *
     * @param editor
     * @param output_message
     * @protected
     */
    insertIntoEditor(editor, output_message) {
        editor.replaceSelection(output_message);
    }
}
OutputChannel_CurrentFileCaret.title = "Current file: caret position";
OutputChannel_CurrentFileCaret.hotkey_letter = "R";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_CurrentFileTop extends OutputChannel_CurrentFile {
    /**
     * Inserts text into the given editor, at top.
     *
     * @param editor
     * @param output_message
     * @protected
     */
    insertIntoEditor(editor, output_message) {
        const top_position = editor.offsetToPos(0);
        editor.replaceRange(output_message, top_position);
    }
}
OutputChannel_CurrentFileTop.title = "Current file: top";
OutputChannel_CurrentFileTop.hotkey_letter = "T";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_StatusBar extends OutputChannel {
    constructor() {
        super(...arguments);
        /**
         * All received output cumulatively. Subsequent handlings will then use the whole output, not just new parts.
         * Only used in "realtime" mode.
         *
         * @private
         */
        this.realtimeContentBuffer = "";
    }
    async _handleBuffered(outputContent) {
        this.setStatusBarContent(outputContent);
    }
    async _handleRealtime(outputContent) {
        this.realtimeContentBuffer += outputContent;
        this.setStatusBarContent(this.realtimeContentBuffer);
    }
    setStatusBarContent(outputContent) {
        const status_bar_element = this.plugin.getOutputStatusBarElement();
        outputContent = outputContent.trim();
        // Full output (shown when hovering with mouse)
        status_bar_element.setAttr("aria-label", outputContent);
        // FIXME: Make the statusbar element support HTML content better. Need to:
        //  - Make the hover content appear in a real HTML element, not in aria-label="" attribute.
        //  - Ensure the always visible bottom line contains all formatting that might come from lines above it.
        // Show last line permanently.
        const output_message_lines = outputContent.split(/(\r\n|\r|\n|<br>)/u); // <br> is here just in case, haven't tested if ansi_up adds it or not.
        const last_output_line = output_message_lines[output_message_lines.length - 1];
        status_bar_element.setText(obsidian.sanitizeHTMLToDom(last_output_line));
    }
}
OutputChannel_StatusBar.title = "Status bar";
OutputChannel_StatusBar.accepts_empty_output = true;
OutputChannel_StatusBar.hotkey_letter = "S";
/**
 * Combine stdout and stderr (in case both of them happen to be present).
 * @protected
 */
OutputChannel_StatusBar.combine_output_streams = os.EOL + os.EOL;

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_CurrentFileBottom extends OutputChannel_CurrentFile {
    /**
     * Inserts text into the given editor, at bottom.
     *
     * @param editor
     * @param output_message
     * @protected
     */
    insertIntoEditor(editor, output_message) {
        const bottom_position = {
            ch: editor.getLine(editor.lastLine()).length,
            line: editor.lastLine(), // ... the last line.
        }; // *) But do not subtract 1, because ch is zero-based, so when .length is used without -1, we are pointing AFTER the last character.
        editor.replaceRange(output_message, bottom_position);
    }
}
OutputChannel_CurrentFileBottom.title = "Current file: bottom";
OutputChannel_CurrentFileBottom.hotkey_letter = "B";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_Clipboard extends OutputChannel {
    constructor() {
        super(...arguments);
        /**
         * All received output cumulatively. Subsequent handlings will then use the whole output, not just new parts.
         * Only used in "realtime" mode.
         *
         * @private
         */
        this.realtimeContentBuffer = "";
    }
    async _handleBuffered(outputContent) {
        await copyToClipboard(outputContent);
        this.notify(outputContent);
    }
    async _handleRealtime(outputContent) {
        this.realtimeContentBuffer += outputContent;
        await copyToClipboard(this.realtimeContentBuffer);
        this.notify(this.realtimeContentBuffer);
    }
    notify(output_message) {
        if (this.plugin.settings.output_channel_clipboard_also_outputs_to_notification) {
            // Notify the user so they know a) what was copied to clipboard, and b) that their command has finished execution.
            this.plugin.newNotification("Copied to clipboard: " + os.EOL + output_message + os.EOL + os.EOL + "(Notification can be turned off in settings.)");
        }
    }
}
OutputChannel_Clipboard.title = "Clipboard";
OutputChannel_Clipboard.hotkey_letter = "L";
/**
 * There can be both "stdout" and "stderr" present at the same time, or just one of them. If both are present, they
 * will be joined together with " " as a separator.
 * @protected
 */
OutputChannel_Clipboard.combine_output_streams = " "; // TODO: Change to "" as there should be no extra space between stdout and stderr. Compare it to the terminal: AFAIK there is no separation between stdout and stderr outputs, just that typically each output ends with a newline.

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * TODO: Move this to TShellCommand.
 */
function getHotkeysForShellCommand(plugin, shell_command_id) {
    // Retrieve all hotkeys set by user.
    // @ts-ignore PRIVATE API
    const app_custom_hotkeys = plugin.app.hotkeyManager?.customKeys;
    if (!app_custom_hotkeys) {
        debugLog("getHotkeysForShellCommand() failed, will return an empty array.");
        return [];
    }
    // Get only our hotkeys.
    const hotkey_index = plugin.getPluginId() + ":" + plugin.generateObsidianCommandId(shell_command_id); // E.g. "obsidian-shellcommands:shell-command-0"
    debugLog("getHotkeysForShellCommand() succeeded.");
    return app_custom_hotkeys[hotkey_index] ?? []; // If no hotkey array is set for this command, return an empty array. Although I do believe that all commands do have an array anyway, but have this check just in case.
}
/**
 * TODO: Is there a way to make Obsidian do this conversion for us? Check this: https://github.com/pjeby/hotkey-helper/blob/c8a032e4c52bd9ce08cb909cec15d1ed9d0a3439/src/plugin.js#L4-L6
 *
 * @param hotkey
 * @constructor
 */
function HotkeyToString(hotkey) {
    const keys = [];
    hotkey.modifiers.forEach((modifier) => {
        let modifier_key = modifier.toString(); // This is one of 'Mod' | 'Ctrl' | 'Meta' | 'Shift' | 'Alt'
        if ("Mod" === modifier_key) {
            // Change "Mod" to something more meaningful.
            modifier_key = CmdOrCtrl(); // isMacOS should also be true if the device is iPhone/iPad. Can be handy if this plugin gets mobile support some day.
        }
        keys.push(modifier_key);
    });
    keys.push(hotkey.key); // This is something like a letter ('A', 'B' etc) or space/enter/whatever.
    return keys.join(" + ");
}
function CmdOrCtrl() {
    return obsidian.Platform.isMacOS ? "Cmd" : "Ctrl";
}
function isCmdOrCtrlPressed(event) {
    if (obsidian.Platform.isMacOS) {
        return event.metaKey;
    }
    else {
        return event.ctrlKey;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_Modal extends OutputChannel {
    initialize() {
        // Initialize a modal (but don't open yet)
        this.modal = new OutputModal(this.plugin, this.t_shell_command, this.shell_command_parsing_result, this.processTerminator);
    }
    async _handleBuffered(outputs, error_code) {
        // Pass outputs to modal
        this.modal.setOutputContents(outputs);
        // Define a possible error code to be shown on the modal.
        if (error_code !== null) {
            this.modal.setExitCode(error_code);
        }
        // Done
        this.modal.open();
    }
    async _handleRealtime(outputContent, outputStreamName) {
        this.modal.addOutputContent(outputStreamName, outputContent);
        if (!this.modal.isOpen()) {
            this.modal.open();
        }
    }
    /**
     * @param exitCode Can be null if user terminated the process by clicking a button. In other places exitCode can be null if process is still running, but here that cannot be the case.
     *
     * @protected
     */
    _endRealtime(exitCode) {
        // Delete terminator button as the process is already ended.
        this.modal.removeProcessTerminatorButton();
        // Pass exitCode to the modal
        this.modal.setExitCode(exitCode);
    }
}
OutputChannel_Modal.title = "Ask after execution";
class OutputModal extends SC_Modal {
    constructor(plugin, t_shell_command, shell_command_parsing_result, processTerminator) {
        super(plugin);
        this.processTerminator = processTerminator;
        this.exit_code = null; // TODO: Think about changing the logic: exit code could be undefined when it's not received, and null when a user has terminated the execution. The change needs to be done in the whole plugin, although I only wrote about it in this OutputModal class.
        this.t_shell_command = t_shell_command;
        this.shell_command_parsing_result = shell_command_parsing_result;
        this.createOutputFields();
    }
    /**
     * Called when doing "buffered" output handling.
     *
     * @param outputs
     */
    setOutputContents(outputs) {
        Object.getOwnPropertyNames(outputs).forEach((outputStreamName) => {
            const outputField = this.outputFields[outputStreamName];
            // Set field value
            const textareaComponent = outputField.components.first();
            const outputContent = outputs[outputStreamName];
            textareaComponent.setValue(outputContent); // as string = outputContent is not undefined because of the .forEach() loop.
            // Make field visible (if it's not already)
            outputField.settingEl.matchParent(".SC-hide")?.removeClass("SC-hide");
        });
    }
    /**
     * Called when doing "realtime" output handling.
     *
     * @param outputStreamName
     * @param outputContent
     */
    addOutputContent(outputStreamName, outputContent) {
        const outputField = this.outputFields[outputStreamName];
        // Update field value
        const textareaComponent = outputField.components.first();
        textareaComponent.setValue(textareaComponent.getValue() + outputContent);
        // Make field visible (if it's not already)
        outputField.settingEl.matchParent(".SC-hide")?.removeClass("SC-hide");
    }
    onOpen() {
        super.onOpen();
        this.modalEl.addClass("SC-modal-output");
        // Heading
        const heading = this.shell_command_parsing_result.alias;
        this.titleEl.innerText = heading ? heading : "Shell command output"; // TODO: Use this.setTitle() instead.
        // Shell command preview
        this.modalEl.createEl("pre", {
            text: this.shell_command_parsing_result.unwrappedShellCommandContent,
            attr: { class: "SC-no-margin SC-wrappable" }
        } // No margin so that exit code will be near.
        );
        // Container for terminating button and exit code
        const processResultContainer = this.modalEl.createDiv();
        // 'Request to terminate the process' icon button
        if (this.processTerminator) {
            this.processTerminatorButtonContainer = processResultContainer.createEl('span');
            this.plugin.createRequestTerminatingButton(this.processTerminatorButtonContainer, this.processTerminator);
        }
        // Exit code (put on same line with process terminator button, if exists)
        this.exitCodeElement = processResultContainer.createEl("small", { text: "Executing...", attr: { style: "font-weight: bold;" } }); // Show "Executing..." before an actual exit code is received.
        if (this.exit_code !== null) {
            this.displayExitCode();
        }
        // Output fields
        this.modalEl.insertAdjacentElement("beforeend", this.outputFieldsContainer);
        // Focus on the first output field
        this.focusFirstField();
        // A tip about selecting text.
        this.modalEl.createDiv({
            text: "Tip! If you select something, only the selected text will be used.",
            attr: { class: "setting-item-description" /* A CSS class defined by Obsidian. */ },
        });
    }
    createOutputFields() {
        // Create a parent-less container. onOpen() will place it in the correct place.
        this.outputFieldsContainer = document.createElement('div');
        // Create field containers in correct order
        let stdoutFieldContainer;
        let stderrFieldContainer;
        switch (this.t_shell_command.getOutputChannelOrder()) {
            case "stdout-first": {
                stdoutFieldContainer = this.outputFieldsContainer.createDiv();
                stderrFieldContainer = this.outputFieldsContainer.createDiv();
                break;
            }
            case "stderr-first": {
                stderrFieldContainer = this.outputFieldsContainer.createDiv();
                stdoutFieldContainer = this.outputFieldsContainer.createDiv();
                break;
            }
        }
        // Create fields
        this.outputFields = {
            stdout: this.createOutputField("stdout", stdoutFieldContainer),
            stderr: this.createOutputField("stderr", stderrFieldContainer),
        };
        // Define hotkeys.
        const outputChannelClasses = getOutputChannelClasses();
        for (const outputChannelName of Object.getOwnPropertyNames(outputChannelClasses)) {
            const outputChannelClass = outputChannelClasses[outputChannelName];
            // Ensure this channel is not excluded by checking that is has a hotkey defined.
            if (outputChannelClass.hotkey_letter) {
                const hotkeyHandler = () => {
                    const activeTextarea = this.getActiveTextarea();
                    if (activeTextarea) {
                        this.redirectOutput(outputChannelName, activeTextarea.outputStreamName, activeTextarea.textarea);
                    }
                };
                // 1. hotkey: Ctrl/Cmd + number: handle output.
                this.scope.register(["Ctrl"], outputChannelClass.hotkey_letter, hotkeyHandler);
                // 2. hotkey: Ctrl/Cmd + Shift + number: handle output and close the modal.
                this.scope.register(["Ctrl", "Shift"], outputChannelClass.hotkey_letter, () => {
                    hotkeyHandler();
                    this.close();
                });
            }
        }
        // Hide the fields' containers at the beginning. They will be shown when content is added.
        stdoutFieldContainer.addClass("SC-hide");
        stderrFieldContainer.addClass("SC-hide");
    }
    createOutputField(output_stream, containerElement) {
        containerElement.createEl("hr", { attr: { class: "SC-no-margin" } });
        // Output stream name
        new obsidian.Setting(containerElement)
            .setName(output_stream)
            .setHeading()
            .setClass("SC-no-bottom-border");
        // Textarea
        const textarea_setting = new obsidian.Setting(containerElement)
            .addTextArea(() => { }) // No need to do anything, but the method requires a callback.
        ;
        textarea_setting.infoEl.addClass("SC-hide"); // Make room for the textarea by hiding the left column.
        textarea_setting.settingEl.addClass("SC-output-channel-modal-textarea-container", "SC-no-top-border");
        // Add controls for redirecting the output to another channel.
        const redirect_setting = new obsidian.Setting(containerElement)
            .setDesc("Redirect:")
            .setClass("SC-no-top-border")
            .setClass("SC-output-channel-modal-redirection-buttons-container") // I think this calls actually HTMLDivElement.addClass(), so it should not override the previous .setClass().
        ;
        const outputChannels = getOutputChannelClasses();
        Object.getOwnPropertyNames(outputChannels).forEach((output_channel_name) => {
            const outputChannelClass = outputChannels[output_channel_name];
            // Ensure this channel is not excluded by checking that is has a hotkey defined.
            if (outputChannelClass.hotkey_letter) {
                // Ensure the output channel accepts this output stream. E.g. OutputChannel_OpenFiles does not accept "stderr".
                if (outputChannelClass.acceptsOutputStream(output_stream)) {
                    const textarea_element = textarea_setting.settingEl.find("textarea");
                    // Create the button
                    redirect_setting.addButton((button) => {
                        button.onClick(async (event) => {
                            // Handle output
                            await this.redirectOutput(output_channel_name, output_stream, textarea_element);
                            // Finish
                            if (isCmdOrCtrlPressed(event)) {
                                // Special click, control/command key is pressed.
                                // Close the modal.
                                this.close();
                            }
                            else {
                                // Normal click, control key is not pressed.
                                // Do not close the modal.
                                textarea_element.focus(); // Bring the focus back to the textarea in order to show a possible highlight (=selection) again.
                            }
                        });
                        // Define button texts and assign hotkeys
                        const output_channel_title = outputChannelClass.getTitle(output_stream);
                        // Button text
                        button.setButtonText(output_channel_title);
                        // Tips about hotkeys
                        button.setTooltip(`Redirect: Normal click OR ${CmdOrCtrl()} + ${outputChannelClass.hotkey_letter}.`
                            + os.EOL + os.EOL +
                            `Redirect and close the modal: ${CmdOrCtrl()} + click OR ${CmdOrCtrl()} + Shift + ${outputChannelClass.hotkey_letter}.`);
                    });
                }
            }
        });
        return textarea_setting;
    }
    async redirectOutput(outputChannelName, outputStreamName, sourceTextarea) {
        const outputContent = getSelectionFromTextarea(sourceTextarea, true) // Use the selection, or...
            ?? sourceTextarea.value // ...use the whole text, if nothing is selected.
        ;
        const output_streams = {
            [outputStreamName]: outputContent,
        };
        const outputChannel = initializeOutputChannel(outputChannelName, this.plugin, this.t_shell_command, this.shell_command_parsing_result, "buffered", // Use "buffered" mode even if this modal was opened in "realtime" mode, because at this point the output redirection is a single-time job, not recurring.
        this.processTerminator);
        await outputChannel.handleBuffered(output_streams, this.exit_code, false); // false: Disable output wrapping as it's already wrapped before the output content was passed to this modal.
    }
    /**
     * Looks up for a <textarea> which meets the following criteria:
     *  1) The <textarea> is visible, AND
     *  2) The <textarea> is either:
     *      a) the only <textarea> in this modal, OR
     *      b) having focus.
     * If no <textarea> is found that meets the requirements, the method shows an error message to user and returns null.
     * @private
     */
    getActiveTextarea() {
        // Pick a <textarea> and output stream name (i.e. `stdout` or `stderr`)
        const activeTextareaCandidates = [];
        for (const outputStreamName of Object.getOwnPropertyNames(this.outputFields)) {
            const outputSetting = this.outputFields[outputStreamName];
            const component = outputSetting.components.first();
            if (component instanceof obsidian.TextAreaComponent) {
                if (component.inputEl.isShown()) {
                    // The textarea is visible. I.e. it doesn't have "SC-hide" CSS class.
                    activeTextareaCandidates.push({
                        textarea: component.inputEl,
                        outputStreamName: outputStreamName,
                    });
                }
            }
        }
        switch (activeTextareaCandidates.length) {
            case 0:
                // No textarea is visible. I'm not sure if this can ever happen? Maybe in "realtime" output mode, but in that mode output modal should only appear after there is some output received.
                this.plugin.newError("No output fields are present, so cannot redirect output.");
                return null;
            case 1:
                // Just one visible textarea exists. Easy pick!
                return activeTextareaCandidates[0];
            default:
                // Multiple visible textareas exist.
                // Try to find one with focus.
                for (const activeTextareaCandidate of activeTextareaCandidates) {
                    if (activeTextareaCandidate.textarea.matches(":focus")) {
                        // Found a focused textarea.
                        return activeTextareaCandidate;
                    }
                }
                // None of the multiple textareas has focus.
                this.plugin.newError("Multiple output fields are present, please put focus on the one you'd like to use.");
                return null;
        }
    }
    removeProcessTerminatorButton() {
        if (this.processTerminatorButtonContainer) {
            this.processTerminatorButtonContainer.remove();
        }
    }
    /**
     * Should be called only if an exit code was received.
     *
     * @param exit_code Can be null if user terminated the process by clicking a button.
     */
    setExitCode(exit_code) {
        this.exit_code = exit_code;
        // Try to show the exit code.
        if (this.isOpen()) {
            if (null === this.exit_code) {
                // User has terminated the process, so there's no exit code even though the process has ended.
                this.exitCodeElement.innerText = "User terminated";
            }
            else {
                // displayExistCode() can only be called if onOpen() has been called before.
                // If onOpen() will be called later, it will call displayExitCode() itself when it sees that this.exit_code is defined.
                this.displayExitCode();
            }
        }
    }
    displayExitCode() {
        if (null === this.exit_code) {
            // Currently there are two callers for this method, and both of them does a null check on the exit code before'
            // the call, so we'll never get here in practise.
            // TODO: Remove this checking/throwing and make this method able to display three texts: a) an exit code, b) Executing..., or c) User terminated.
            throw new Error("Cannot display exit code because it's null");
        }
        this.exitCodeElement.innerText = "Exit code: " + this.exit_code.toString();
    }
    focusFirstField() {
        switch (this.t_shell_command.getOutputChannelOrder()) {
            case "stdout-first": {
                this.outputFields.stdout.controlEl.find("textarea").focus();
                break;
            }
            case "stderr-first": {
                this.outputFields.stderr.controlEl.find("textarea").focus();
                break;
            }
        }
    }
    approve() {
        // No need to perform any action, just close the modal.
        this.close();
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_OpenFiles extends OutputChannel {
    _handleBuffered(output, error_code) {
        let handlingPipeline = Promise.resolve();
        let output_stream_name;
        for (output_stream_name in output) {
            handlingPipeline = handlingPipeline.finally(() => {
                return this.handle(output[output_stream_name]);
            });
        }
        return handlingPipeline;
    }
    _handleRealtime(outputContent) {
        return this.handle(outputContent);
    }
    handle(outputContent) {
        return new Promise((resolve) => {
            // Read file definitions. Usually there's just one, but there can be many. Definitions are separated by newline
            // characters. Each file definition defines one file to be opened.
            const file_definitions_string = outputContent.trim(); // Contains at least file name(s), and MAYBE: a caret position, new pane option, and view state
            const file_definitions = file_definitions_string.split(/[\r\n]+/u);
            // Iterate all file definitions that should be opened.
            let opening_pipeline = Promise.resolve();
            for (const file_definition of file_definitions) {
                // Chain each file opening to happen one after another. If one file opening fails for whatever reason, it
                // is ok to continue to open the next file. This is why .finally() is used instead of .then().
                opening_pipeline = opening_pipeline.finally(() => {
                    return this.interpretFileOpeningDefinition(file_definition);
                });
            }
            opening_pipeline.finally(() => resolve() /* Tell that all openings have been processed. */);
        });
    }
    interpretFileOpeningDefinition(file_definition) {
        return new Promise((resolve, reject) => {
            debugLog("OutputChannel_OpenFiles: Interpreting file opening definition: " + file_definition);
            // Get parts that define different details about how the file should be opened
            const file_definition_parts = file_definition.split(":"); // If file_definition is "", i.e. an empty string, the result will be [""], i.e. an array with an empty string as its only item.
            // The first part is always the file path
            let open_file_path = file_definition_parts.shift(); // If file_definition is "", this will be "", too. 'as string' is used because file_definition_parts is never empty (it always contains at least one item), so .shift() will never return undefined here.
            // On Windows: Check if an absolute path was split incorrectly. (E.g. a path starting with "C:\...").
            if (isWindows() && file_definition_parts.length > 0) {
                const combined_path = open_file_path + ":" + file_definition_parts[0];
                if (path__namespace.isAbsolute(combined_path)) {
                    // Yes, the first two parts do form an absolute path together, so they should not be split.
                    open_file_path = combined_path;
                    file_definition_parts.shift(); // Remove the second part so that it won't be accidentally processed in the 'Special features' part.
                }
            }
            // Trim the file path, for being able to use cleaner separation between file name and other parts, e.g: MyFile.md : new-pane
            open_file_path = open_file_path.trim();
            // Special features
            const caret_parts = []; // If caret position is present in file_definition_parts, the first item in this array will be the caret line, the second will be the column. If more parts are present, they will be used for making selections.
            let newLeaf = false;
            let can_create_file = false;
            let file_definition_interpreting_failed = false;
            file_definition_parts.forEach((file_definition_part) => {
                file_definition_part = file_definition_part.toLocaleLowerCase().trim(); // .trim() is for being able to use cleaner separation between e.g. different selections: MyFile.md:1:1:1:-1 : 5:1:5:-1
                // Determine the part type
                if (isInteger(file_definition_part, true)) {
                    // This is a number, so consider it as a caret position part.
                    caret_parts.push(parseInt(file_definition_part));
                }
                else {
                    const multipleNewPartsErrorMessage = "Cannot open file: Only one of the following can be defined: new-pane, new-tab, or new-window.";
                    switch (file_definition_part) {
                        case "new-pane":
                            // Ensure no new-* definition is used before.
                            if (newLeaf === false) {
                                newLeaf = "split";
                            }
                            else {
                                this.plugin.newError(multipleNewPartsErrorMessage);
                                file_definition_interpreting_failed = true;
                            }
                            break;
                        case "new-tab":
                            // Ensure no new-* definition is used before.
                            if (newLeaf === false) {
                                newLeaf = "tab";
                            }
                            else {
                                this.plugin.newError(multipleNewPartsErrorMessage);
                                file_definition_interpreting_failed = true;
                            }
                            break;
                        case "new-window":
                            // Ensure no new-* definition is used before.
                            if (newLeaf === false) {
                                newLeaf = "window";
                            }
                            else {
                                this.plugin.newError(multipleNewPartsErrorMessage);
                                file_definition_interpreting_failed = true;
                            }
                            break;
                        case "can-create-file":
                            can_create_file = true;
                            break;
                        default:
                            this.plugin.newError("Cannot open file: Unrecognised definition part: " + file_definition_part + " in " + file_definition);
                            file_definition_interpreting_failed = true;
                    }
                }
            });
            if (file_definition_interpreting_failed) {
                reject();
                return;
            }
            // Ensure the path is relative
            if (path__namespace.isAbsolute(open_file_path)) {
                // The path is absolute.
                // Check if it can be converted to relative.
                const vault_absolute_path = getVaultAbsolutePath(this.app);
                if (open_file_path.toLocaleLowerCase().startsWith(vault_absolute_path.toLocaleLowerCase())) {
                    // Converting to relative is possible
                    open_file_path = open_file_path.substr(vault_absolute_path.length); // Get everything after the point where the vault path ends.
                }
                else {
                    // Cannot convert to relative, because the file does not reside in the vault
                    this.plugin.newError("Cannot open file '" + open_file_path + "' as the path is outside this vault.");
                    reject();
                    return;
                }
            }
            // Clean up the file path
            open_file_path = obsidian.normalizePath(open_file_path); // normalizePath() is used on purpose, instead of normalizePath2(), because backslashes \ should be converted to forward slashes /
            this.openFileInTab(open_file_path, newLeaf, can_create_file).then(() => {
                // The file is now open
                // Check, did we have a caret position available. If not, do nothing.
                const count_caret_parts = caret_parts.length;
                if (count_caret_parts > 0) {
                    // Yes, a caret position was defined in the output.
                    // Ensure the correct amount of caret position parts.
                    // 0 parts: no caret positioning needs to be done (but in this part of code the amount of parts is always greater than 0).
                    // 1 part: caret line is defined, no column.
                    // 2 parts: caret line and column are defined.
                    // 3 parts: NOT ALLOWED.
                    // 4 parts: selection starting position (line, column) and selection end position (line, column) are defined.
                    // 5 parts or more: NOT ALLOWED. Exception: any number of sets of four parts is allowed, i.e. 8 parts, 12 parts, 16 parts etc. are allowed as they can define multiple selections.
                    const error_message_base = "File opened, but caret cannot be positioned due to an incorrect amount (" + count_caret_parts + ") of numeric values in the output: " + file_definition + os.EOL + os.EOL;
                    if (count_caret_parts == 3) {
                        // Incorrect amount of caret parts
                        this.plugin.newError(error_message_base + "Three numeric parts is an incorrect amount, correct would be 1,2 or 4 parts.");
                        reject();
                        return;
                    }
                    else if (count_caret_parts > 4 && count_caret_parts % 4 !== 0) {
                        // Incorrect amount of caret parts
                        this.plugin.newError(error_message_base + "Perhaps too many numeric parts are defined? If more than four parts are defined, make sure to define complete sets of four parts. The amount of numeric parts needs to be dividable by 4.");
                        reject();
                        return;
                    }
                    // Even though the file is already loaded, rendering it may take some time, thus the height of the content may increase.
                    // For this reason, there needs to be a tiny delay before setting the caret position. If the caret position is set immediately,
                    // the caret will be placed in a correct position, but it might be that the editor does not scroll into correct position, so the
                    // caret might be out of the view, even when it's in a correct place. (Obsidian version 0.13.23).
                    window.setTimeout(() => {
                        const editor = getEditor(this.app);
                        if (editor) {
                            if (count_caret_parts >= 4) {
                                // Selection mode
                                // There can be multiple selections defined
                                const selections = [];
                                while (caret_parts.length) {
                                    const fromLine = caret_parts.shift();
                                    const fromColumn = caret_parts.shift();
                                    const toLine = caret_parts.shift();
                                    const toColumn = caret_parts.shift();
                                    if (undefined === fromLine || undefined === fromColumn || undefined === toLine || undefined === toColumn) {
                                        // This should never happen.
                                        throw new Error("Encountered undefined values in fromLine, fromColumn, toLine, and/or toColumn. Strange, because the correct amount of parts in caret_parts was checked beforehand.");
                                    }
                                    selections.push({
                                        anchor: prepareEditorPosition(editor, fromLine, fromColumn),
                                        head: prepareEditorPosition(editor, toLine, toColumn),
                                    });
                                }
                                editor.setSelections(selections);
                            }
                            else {
                                // Simple caret mode
                                const caret_line = caret_parts[0];
                                const caret_column = caret_parts[1] ?? 1;
                                editor.setCursor(prepareEditorPosition(editor, caret_line, caret_column));
                            }
                            // After placing carets / selecting text, have a small delay after allowing to open another file (in case multiple files are opened in a row). This allows the selection to be remembered in the pane's history.
                            window.setTimeout(resolve, 300); // If you change this ADDITIONAL delay, remember to change it in the documentation, too.
                        }
                        else {
                            // No editor
                            this.plugin.newError("File opened, but caret cannot be positioned because no editor was found.");
                            reject();
                        }
                    }, 500); // 500ms is probably long enough even if a new tab is opened (takes more time than opening a file into an existing tab). This can be made into a setting sometime. If you change this, remember to change it in the documentation, too.
                }
                else {
                    // No caret parts exist. All is done now.
                    resolve();
                }
            }, (error_message) => {
                if (typeof error_message === "string") {
                    // Opening the file has failed.
                    this.plugin.newError(error_message);
                }
                else {
                    // Some other runtime error has occurred.
                    throw error_message;
                }
                reject();
            });
        });
    }
    openFileInTab(file_path, newLeaf, can_create_file) {
        // Ensure that the file exists (or can be created)
        const source_path = ""; // TODO: When adding an option for creating new files, read this documentation from Obsidian API's getNewFileParent(): "sourcePath – The path to the current open/focused file, used when the user wants new files to be created “in the same folder”. Use an empty string if there is no active file."
        const file_exists_or_can_be_created = can_create_file || null !== this.app.metadataCache.getFirstLinkpathDest(file_path, source_path);
        if (file_exists_or_can_be_created) {
            // Yes, the file exists (or can be created)
            return this.app.workspace.openLinkText(file_path, source_path, newLeaf);
        }
        else {
            // No, the file does not exist, and it may not be created.
            return Promise.reject("Cannot open file '" + file_path + "', as it does not exist. (If you want to allow file creation, add :can-create-file to the shell command output.)");
        }
    }
}
OutputChannel_OpenFiles.title = "Open files";
OutputChannel_OpenFiles.hotkey_letter = "O";
/**
 * This output channel is not suitable for stderr, as stderr can contain unexpected messages.
 * @protected
 */
OutputChannel_OpenFiles.accepted_output_streams = ["stdout"];
/**
 * When opening files, the output is not supposed to have any ANSI codes, as it's just a list of file names.
 *
 * @protected
 */
OutputChannel_OpenFiles.applicableConfiguration = {
    convert_ansi_code: false,
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function getVariableAutocompleteItems(plugin) {
    if (0 === autocomplete_items.length) {
        plugin.getVariables().forEach((variable) => {
            autocomplete_items.push(...variable.getAutocompleteItems());
        });
    }
    return autocomplete_items;
}
function resetVariableAutocompleteItems() {
    while (autocomplete_items.length) {
        autocomplete_items.pop();
    }
}
const autocomplete_items = [];

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * This class serves as the actual operational variable class for custom variables. It's paired with the CustomVariableInstance class, which acts
 * as a configuration class to handle settings together with CustomVariableModel class.
 */
class CustomVariable extends Variable {
    constructor(plugin, custom_variable_instance) {
        super(plugin);
        this.custom_variable_instance = custom_variable_instance;
        this.value = null; // TODO: When implementing variable types, make this class abstract and let subclasses define the type of this property.
        this.always_available = false;
        /**
         * If the variable's value comes from a password field, it is marked to be cloaked in CustomVariableView.
         *
         * Even though passwords are also cloaked in Prompt previews, it's not triggered by this property.
         *
         * @private
         */
        this.cloak = false;
        this.on_change_callbacks = new Set();
        this.updateProperties();
        debugLog(`Loaded CustomVariable ${this.variable_name}.`);
    }
    async generateValue() {
        if (null === this.value) {
            debugLog(`Custom variable ${this.variable_name} does not have a value yet, and no default value is defined.`);
            this.throw("This custom variable does not have a value yet, and no default value is defined.");
        }
        return this.value;
    }
    /**
     * Tells whether this CustomVariable has an actual value, NOT falling back to any possible default value.
     */
    hasOwnValue() {
        return null !== this.value;
    }
    /**
     * A simpler way to access the value than Variable.getValue(). Both methods can return the value, but Variable.getValue()
     * would require providing a Shell object (which would not be even used because CustomVariables do not need it) and
     * it would return a VariableValueResult object.
     *
     * @return null if not value is yet set for this CustomVariable, or a string otherwise.
     */
    getCustomVariableValue() {
        return this.value;
    }
    shouldCloak() {
        return this.cloak;
    }
    /**
     * TODO: Make it possible to prevent calling onChange callbacks:
     *  - Make it possible to call the callbacks later outside this class.
     *  - This makes it possible to prevent unnecessary CustomVariableView updates when multiple CustomVariables are assigned values in one go (via Shell command URI).
     *  - Store the old value into some kind of history list.
     *  - When calling the callbacks, the current CustomVariable should be passed as a parameter instead of the 'value' and 'old_value' parameters (which can be accessed via the CustomVariable object).
     *
     * @param value
     * @param source
     * @param cloak This should be true, if the value is a password.
     */
    async setValue(value, source, cloak = false) {
        const old_value = this.value;
        debugLog(`CustomVariable ${this.variable_name}: Setting value to: ${value} (old was: ${old_value}).`);
        this.value = value;
        this.cloak = cloak;
        // Display notifications, if applicable.
        this.notifyChange(source);
        // Call the onChange hook.
        await this.callOnChangeCallbacks(value, old_value ?? ""); // Use "" if old_value is null.
    }
    /**
     * Shows a notification balloon after the CustomVariable's value has changed, but only if enabled by settings.
     *
     * @param {string} source - The source of the change: "manual", "uri", or "output".
     * @return {void}
     * @throws {Error} If the CustomVariable value is null after a change.
     */
    notifyChange(source) {
        let doNotify;
        switch (source) {
            case "manual":
                // Never notify about changes that user has made manually, e.g. changes from Prompts.
                doNotify = false;
                break;
            case "uri":
                // Obsidian URI / Shell commands URI.
                doNotify = this.plugin.settings.custom_variables_notify_changes_via.obsidian_uri;
                break;
            case "output":
                // OutputChannel_AssignCustomVariables.
                doNotify = this.plugin.settings.custom_variables_notify_changes_via.output_assignment;
                break;
        }
        if (doNotify) {
            if (this.value === null) {
                throw new Error("CustomVariable value is null after a change.");
            }
            const displayValue = this.cloak ? cloakPassword(this.value) : this.value;
            this.plugin.newNotification(this.getFullName() + " =" + os.EOL + displayValue);
        }
    }
    /**
     * Retrieves variable_name and help_text properties from the associated CustomVariableInstance.
     * Called when loading the CustomVariable and when the associated CustomVariableInstance's settings are changed.
     */
    updateProperties() {
        debugLog(`CustomVariable ${this.variable_name}: Updating variable name and help text.`);
        this.variable_name = this.custom_variable_instance.getPrefixedName();
        this.help_text = this.custom_variable_instance.configuration.description;
        resetVariableAutocompleteItems(); // Make autocomplete lists reload their content in order to get the new variable name/help text.
    }
    getIdentifier() {
        return this.custom_variable_instance.getID();
    }
    /**
     * Adds the given callback function to a stack of functions that will be called whenever this CustomVariable's value changes.
     * @param on_change_callback
     */
    onChange(on_change_callback) {
        this.on_change_callbacks.add(on_change_callback);
    }
    async callOnChangeCallbacks(new_value, old_value) {
        debugLog(`CustomVariable ${this.variable_name}: Calling onChange callbacks.`);
        for (const on_change_callback of this.on_change_callbacks) {
            await on_change_callback(this, new_value, old_value);
        }
    }
    getConfiguration() {
        return this.custom_variable_instance.configuration;
    }
    getGlobalDefaultValueConfiguration() {
        return this.custom_variable_instance.configuration.default_value;
    }
    getCustomVariableInstance() {
        return this.custom_variable_instance;
    }
    static getCustomVariableValidNameRegex(precedingUnderscore) {
        if (precedingUnderscore) {
            return /^_[\w\d]+$/u;
        }
        else {
            return /^[\w\d]+$/u;
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputChannel_AssignCustomVariables extends OutputChannel {
    async _handleBuffered(outputContent) {
        await this.assignCustomVariablesFromJSON(outputContent);
    }
    async _handleRealtime(outputContent) {
        await this.assignCustomVariablesFromJSON(outputContent);
    }
    async assignCustomVariablesFromJSON(variablesJSON) {
        // Parse the JSON.
        debugLog("OutputChannel_AssignCustomVariables: Starting to interpret JSON: ", variablesJSON);
        let assignableVariables;
        try {
            assignableVariables = JSON.parse(variablesJSON);
        }
        catch (exception) {
            if (exception instanceof SyntaxError) {
                // JSON syntax error occurred.
                debugLog("OutputChannel_AssignCustomVariables: Malformed JSON:", variablesJSON);
                this.newError(`JSON syntax error: ${os.EOL}${os.EOL}${exception.message}${os.EOL}${os.EOL}${variablesJSON}`);
                return;
            }
            else {
                throw exception;
            }
        }
        // Check that the JSON contained an object.
        if ("object" !== typeof assignableVariables || assignableVariables === null) {
            debugLog("OutputChannel_AssignCustomVariables: JSON is not an object:", variablesJSON);
            this.newError(`Expected an object like {"{{_myCustomVariable}}": "A value"}, received instead: ${os.EOL}${os.EOL}${variablesJSON}`);
            return;
        }
        // Get all CustomVariables.
        const customVariables = this.plugin.getCustomVariables();
        let showReceivedJSON = false; // Becomes true if any errors occur. In case of errors with multiple variables/JSON properties, avoid showing the whole JSON repeatedly.
        for (const assignableVariableName of Object.getOwnPropertyNames(assignableVariables)) {
            // 1. Check the variable name's format generally.
            if (!assignableVariableName.match(CustomVariable.getCustomVariableValidNameRegex(true))) {
                // Malformed CustomVariable name.
                this.newError(`Property names should be like _myCustomVariable, received instead: ${assignableVariableName}`);
                showReceivedJSON = true;
                continue; // Next variable.
            }
            // 2. Check that the variable exists.
            let variableFound = false;
            for (const customVariable of customVariables) {
                if (!(customVariable instanceof CustomVariable)) {
                    // This should not happen.
                    throw new Error(this.constructor.name + ": Got a variable that is not a CustomVariable.");
                }
                if (customVariable.getFullName() === "{{" + assignableVariableName + "}}") {
                    // Found the variable.
                    variableFound = true;
                    // @ts-ignore assignableVariables[assignableVariableName] is assigned.
                    const assignableVariableValue = assignableVariables[assignableVariableName];
                    if (typeof assignableVariableValue === "string") {
                        // The value can be assigned to the variable.
                        await customVariable.setValue(assignableVariableValue, "output");
                    }
                    else {
                        // The value has a wrong data type.
                        this.newError(`Currently, only string values are supported for custom variables. ${assignableVariableName} was tried to be given this: ${JSON.stringify(assignableVariableValue)}`);
                        showReceivedJSON = true;
                    }
                    break;
                }
            }
            if (!variableFound) {
                this.newError(`Variable with name ${assignableVariableName} does not exist.`);
                showReceivedJSON = true;
                continue; // Next variable.
            }
        }
        if (showReceivedJSON) {
            this.plugin.newError(`Received output JSON:${os.EOL}${variablesJSON}`);
            // This is a second notification balloon, so avoid repeating Assign custom variables:" by not using this.newError().
        }
        debugLog("OutputChannel_AssignCustomVariables: Finished interpreting JSON successfully: ", variablesJSON);
    }
}
OutputChannel_AssignCustomVariables.title = "Assign custom variables";
/**
 * This output channel is not suitable for stderr, as stderr can contain unexpected messages.
 * @protected
 */
OutputChannel_AssignCustomVariables.accepted_output_streams = ["stdout"];
/**
 * There really shouldn't be anything to combine, because stderr is disabled for this output handled. Just use a string
 * here to indicate that _handleBuffered() expects a simple string value, not an OutputStreams object.
 *
 * @protected
 */
OutputChannel_AssignCustomVariables.combine_output_streams = "";
// I guess this output handler is so rarely used it's not needed in the 'Ask after execution' modal. If this decision is changed, "U" could be a candidate.
// public static readonly hotkey_letter = "U";
/**
 * When assigning values to variables, the output is not supposed to have any ANSI codes, as it's just JSON.
 *
 * @protected
 */
OutputChannel_AssignCustomVariables.applicableConfiguration = {
    convert_ansi_code: false, // The output should be just JSON, no ANSI code in there.
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
const outputChannelClasses = {};
// Register output channels
registerOutputChannel("notification", OutputChannel_Notification);
registerOutputChannel("current-file-caret", OutputChannel_CurrentFileCaret);
registerOutputChannel("current-file-top", OutputChannel_CurrentFileTop);
registerOutputChannel("current-file-bottom", OutputChannel_CurrentFileBottom);
registerOutputChannel("open-files", OutputChannel_OpenFiles);
registerOutputChannel("status-bar", OutputChannel_StatusBar);
registerOutputChannel("clipboard", OutputChannel_Clipboard);
registerOutputChannel("assign-custom-variables", OutputChannel_AssignCustomVariables);
registerOutputChannel("modal", OutputChannel_Modal);
/**
 * This function is designed to be called after a 'Wait until finished' type of shell command finishes its execution.
 *
 * @param plugin
 * @param t_shell_command
 * @param shell_command_parsing_result
 * @param stdout
 * @param stderr
 * @param error_code TODO: Rename to exitCode everywhere in the codebase.
 * @param outputHandlers
 */
function handleBufferedOutput(plugin, t_shell_command, shell_command_parsing_result, stdout, stderr, error_code, outputHandlers) {
    // Terminology: Stream = outputs stream from a command, can be "stdout" or "stderr". Channel = a method for this application to present the output ot user, e.g. "notification".
    const shell_command_configuration = t_shell_command.getConfiguration(); // TODO: Refactor OutputChannels to use TShellCommand instead of the configuration objects directly.
    // Insert stdout and stderr to an object in a correct order
    let output = {};
    if (stdout.length && stderr.length) {
        // Both stdout and stderr have content
        // Decide the output order == Find out which data stream should be processed first, stdout or stderr.
        switch (shell_command_configuration.output_channel_order) {
            case "stdout-first":
                output = {
                    stdout: stdout,
                    stderr: stderr,
                };
                break;
            case "stderr-first":
                output = {
                    stderr: stderr,
                    stdout: stdout,
                };
                break;
        }
    }
    else if (stdout.length) {
        // Only stdout has content
        output = {
            stdout: stdout,
        };
    }
    else if (stderr.length) {
        // Only stderr has content
        output = {
            stderr: stderr,
        };
    }
    else {
        // Neither stdout nor stderr have content
        // Provide empty output, some output channels will process it, while other will just ignore it.
        output = {
            "stdout": "",
        };
    }
    // Should stderr be processed same time with stdout?
    if (outputHandlers.stdout.handler === outputHandlers.stderr.handler) {
        // Stdout and stderr use the same channel.
        // Make one handling call.
        handle_stream(plugin, t_shell_command, shell_command_parsing_result, outputHandlers.stdout.handler, output, error_code);
    }
    else {
        // Stdout and stderr use different channels.
        // Make two handling calls.
        let output_stream_name;
        for (output_stream_name in output) {
            const output_channel_name = outputHandlers[output_stream_name].handler;
            const output_message = output[output_stream_name];
            const separated_output = {};
            separated_output[output_stream_name] = output_message;
            handle_stream(plugin, t_shell_command, shell_command_parsing_result, output_channel_name, separated_output, error_code);
        }
    }
}
async function handle_stream(plugin, t_shell_command, shell_command_parsing_result, output_channel_name, output, error_code) {
    // Check if the output should be ignored
    if ("ignore" !== output_channel_name) {
        // The output should not be ignored.
        // Check that an output channel class exists
        if (undefined === outputChannelClasses[output_channel_name]) {
            throw new Error("No output channel class found for channel '" + output_channel_name + "'.");
        }
        // Instantiate the channel
        const outputChannel = initializeOutputChannel(output_channel_name, plugin, t_shell_command, shell_command_parsing_result, "buffered", null);
        // Perform handling the output
        await outputChannel.handleBuffered(output, error_code);
    }
}
function startRealtimeOutputHandling(plugin, tShellCommand, shellCommandParsingResult, outputHandlerConfigurations, processTerminator) {
    const outputChannels = {};
    // stdout
    if ("ignore" !== outputHandlerConfigurations.stdout.handler) {
        outputChannels.stdout = initializeOutputChannel(outputHandlerConfigurations.stdout.handler, plugin, tShellCommand, shellCommandParsingResult, "realtime", processTerminator);
    }
    // stderr
    if ("ignore" !== outputHandlerConfigurations.stderr.handler) {
        if (outputHandlerConfigurations.stderr.handler === outputHandlerConfigurations.stdout.handler) {
            // stderr should use the same channel instance as stdout.
            outputChannels.stderr = outputChannels.stdout;
        }
        else {
            // stderr uses a different channel than stdout.
            outputChannels.stderr = initializeOutputChannel(outputHandlerConfigurations.stderr.handler, plugin, tShellCommand, shellCommandParsingResult, "realtime", processTerminator);
        }
    }
    return outputChannels;
}
function getOutputChannelsOptionList(output_stream) {
    const list = { ignore: "Ignore" };
    for (const name in outputChannelClasses) {
        const channelClass = outputChannelClasses[name];
        // Check that the stream is suitable for the channel
        if (channelClass.acceptsOutputStream(output_stream)) {
            list[name] = channelClass.getTitle(output_stream);
        }
    }
    return list;
}
function getOutputChannelClasses() {
    return outputChannelClasses;
}
function initializeOutputChannel(channelCode, plugin, tShellCommand, shellCommandParsingResult, outputHandlingMode, processTerminator) {
    // @ts-ignore TODO: Find out how to tell TypeScript that a subclass is being instatiated instead of the abstract base class:
    return new outputChannelClasses[channelCode](plugin, tShellCommand, shellCommandParsingResult, outputHandlingMode, processTerminator);
}
function registerOutputChannel(channelCode, channelClass) {
    if (undefined !== outputChannelClasses[channelCode]) {
        throw new Error("OutputChannel named '" + channelCode + "' is already registered!");
    }
    outputChannelClasses[channelCode] = channelClass;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_onLayoutReady extends SC_Event {
    constructor() {
        super(...arguments);
        this.register_after_changing_settings = false;
    }
    _register(t_shell_command) {
        this.app.workspace.onLayoutReady(async () => await this.trigger(t_shell_command));
        return false; // The base class does not need to register anything.
    }
    _unregister(t_shell_command) {
        // No need to unregister, because this event happens only once when Obsidian starts. If the event is not enabled for a shell command, next time Obsidian starts, this event won't get registered.
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "application-started";
    }
    getCategory() {
        return "application";
    }
}
SC_Event_onLayoutReady.event_code = "on-layout-ready";
SC_Event_onLayoutReady.event_title = "Obsidian starts";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_onQuit extends SC_WorkspaceEvent {
    constructor() {
        super(...arguments);
        this.workspace_event = "quit";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "application-quit";
    }
    getCategory() {
        return "application";
    }
}
SC_Event_onQuit.event_code = "on-quit";
SC_Event_onQuit.event_title = "Obsidian quits";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_onActiveLeafChanged extends SC_WorkspaceEvent {
    constructor() {
        super(...arguments);
        this.workspace_event = "active-leaf-change";
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "active-leaf-switched";
    }
}
SC_Event_onActiveLeafChanged.event_code = "on-active-leaf-changed";
SC_Event_onActiveLeafChanged.event_title = "Switching the active pane";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_EveryNSeconds extends SC_Event {
    constructor() {
        super(...arguments);
        this.default_configuration = {
            enabled: false,
            seconds: 60,
        };
        this.intervals_ids = {};
    }
    _register(t_shell_command) {
        const milliseconds = this.getConfiguration(t_shell_command).seconds * 1000;
        const interval_id = window.setInterval(async () => await this.trigger(t_shell_command), milliseconds);
        this.plugin.registerInterval(interval_id);
        this.intervals_ids[t_shell_command.getId()] = interval_id;
        return false; // The base class does not need to register anything.
    }
    _unregister(t_shell_command) {
        window.clearInterval(this.intervals_ids[t_shell_command.getId()]);
    }
    /**
     * Overridden only to change the return type.
     * @param t_shell_command
     * @protected
     */
    getConfiguration(t_shell_command) {
        return super.getConfiguration(t_shell_command);
    }
    createExtraSettingsFields(extra_settings_container, t_shell_command) {
        const configuration = this.getConfiguration(t_shell_command);
        let apply_seconds;
        new obsidian.Setting(extra_settings_container)
            .setName("Seconds")
            .setDesc("Needs to be over 0. One decimal is supported.")
            .addText(text => text
            .setValue(configuration.seconds.toString())
            .onChange((rawSeconds) => {
            apply_seconds = inputToFloat(rawSeconds, 1);
            // Don't save here, because the user might still be editing the number.
        }))
            .addButton(button => button
            .setButtonText("Apply")
            .onClick(async () => {
            if (undefined == apply_seconds || apply_seconds === this.getConfiguration(t_shell_command).seconds) {
                new obsidian.Notice("You didn't change the seconds!");
            }
            else if (apply_seconds <= 0) {
                new obsidian.Notice("The seconds need to be over 0!");
            }
            else {
                // All ok, save.
                this.getConfiguration(t_shell_command).seconds = apply_seconds;
                await this.plugin.saveSettings();
                // Re-register to apply the change
                this.unregister(t_shell_command);
                this.register(t_shell_command);
                // Done
                this.noticeAboutEnabling(t_shell_command);
            }
        }));
    }
    onAfterEnabling(t_shell_command) {
        this.noticeAboutEnabling(t_shell_command);
    }
    noticeAboutEnabling(t_shell_command) {
        new obsidian.Notice("The shell command will run every " + this.getConfiguration(t_shell_command).seconds + " seconds");
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "every-n-seconds";
    }
    getCategory() {
        return "time";
    }
}
SC_Event_EveryNSeconds.event_code = "every-n-seconds";
SC_Event_EveryNSeconds.event_title = "Every n seconds";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_EditorMenu extends SC_MenuEvent {
    constructor() {
        super(...arguments);
        this.workspace_event = "editor-menu";
    }
    getTrigger(t_shell_command) {
        return async (menu, editor, view) => {
            await this.addTShellCommandToMenu(t_shell_command, menu);
        };
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "editor-menu-item";
    }
}
SC_Event_EditorMenu.event_code = "editor-menu";
SC_Event_EditorMenu.event_title = "Editor menu";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_CodeMirrorEvent extends SC_Event {
    constructor() {
        super(...arguments);
        /**
         * Contains a trigger callback for each TShellCommand that has this event enabled.
         *
         * @private
         */
        this.registeredShellCommands = new Map();
        /**
         * As multiple shell commands can enable an event, this property indicates whether a CodeMirror extension is already registered or not.
         * @private
         */
        this.isCodeMirrorExtensionRegistered = false;
    }
    _register(tShellCommand) {
        this.registeredShellCommands.set(tShellCommand.getId(), tShellCommand);
        if (!this.isCodeMirrorExtensionRegistered) {
            // Need to register the CodeMirror extension. Only need to do this once per event, not for every shell command using the same event.
            this.plugin.addEditorExtension(this.getCodeMirrorExtension());
            this.isCodeMirrorExtensionRegistered = true;
        }
        return false; // No event reference.
    }
    _unregister(tShellCommand) {
        this.registeredShellCommands.delete(tShellCommand.getId());
        // No need to unregister the CodeMirror extension. It might be used by other shell commands, and even if it's not, leaving it registered causes no harm.
    }
    /**
     * Subclasses of SC_CodeMirrorEvent should call this when their event occurs.
     * @param condition Can be used to filter which shell commands should be executed and which not. If omitted, all shell
     * commands (that enable this event) will be executed.
     */
    triggerRegisteredShellCommands(condition) {
        for (const tShellCommand of this.registeredShellCommands.values()) {
            if (!condition || condition(tShellCommand)) {
                this.trigger(tShellCommand).then();
            }
        }
    }
    getCategory() {
        return "editor";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Event_CaretMoved extends SC_CodeMirrorEvent {
    constructor() {
        super(...arguments);
        this.default_configuration = {
            enabled: false,
            lineOrColumn: "any",
        };
    }
    getCodeMirrorExtension() {
        return view.EditorView.updateListener.of((update) => {
            if (update.selectionSet) {
                // Selection/caret position has changed.
                if (!update.state.selection.eq(update.startState.selection)) { // Prevent double triggering when clicking with mouse. This discards mouse button RELEASE.
                    // What has changed: line, column, or both?
                    const lineAndColumnNumbers = getCodeMirrorLineAndColumnNumbers(update);
                    const lineChanged = lineAndColumnNumbers.old.line !== lineAndColumnNumbers.new.line;
                    const columnChanged = lineAndColumnNumbers.old.column !== lineAndColumnNumbers.new.column;
                    // Execute shell commands - but with filtering.
                    this.triggerRegisteredShellCommands((tShellCommand) => {
                        // Check which kind of change the shell command requires.
                        const lineOrColumn = this.getConfiguration(tShellCommand).lineOrColumn;
                        switch (lineOrColumn) {
                            case "any":
                                return columnChanged || lineChanged;
                            case "column":
                                return columnChanged;
                            case "line":
                                return lineChanged;
                            default:
                                // There might be some < 0.22.0 configurations out there that does have `caret-moved` event enabled (even though the event does not work before version 0.22.0), but have no `lineOrColumn` property defined.
                                // TODO: Create a migration in Migrations.ts that adds the lineOrColumn property to possibly existing caret moved event configurations. The migration could be generalised in a way that it could insert properties to any events that might be missing properties.
                                return columnChanged || lineChanged;
                        }
                    });
                }
            }
        });
    }
    createExtraSettingsFields(extraSettingsContainer, tShellCommand) {
        // Line or column mode.
        new obsidian.Setting(extraSettingsContainer)
            .setName("React to line or column changes")
            .addDropdown(lineOrColumnDropdown => lineOrColumnDropdown
            .addOptions({
            any: "Any changes",
            line: "Line changes only",
            column: "Column changes only",
        })
            .setValue(this.getConfiguration(tShellCommand).lineOrColumn)
            .onChange(async (newMode) => {
            this.getConfiguration(tShellCommand).lineOrColumn = newMode;
            await this.plugin.saveSettings();
        }));
    }
    /**
     * Overridden only to change the return type.
     * @param tShellCommand
     * @protected
     */
    getConfiguration(tShellCommand) {
        return super.getConfiguration(tShellCommand);
    }
    getType() {
        // TODO: Change all event_code properties to be the same as event types, and then make the parent method SC_Event.getType() return event_code. Then all sub-methods of getType() can be removed.
        return "caret-moved";
    }
}
SC_Event_CaretMoved.event_code = "caret-moved";
SC_Event_CaretMoved.event_title = "Caret moved in editor";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function getSC_Events(plugin) {
    if (eventList.length === 0) {
        // Cache the list of SC_Event objects
        eventList.push(new SC_Event_onLayoutReady(plugin), new SC_Event_onQuit(plugin), new SC_Event_onActiveLeafChanged(plugin), new SC_Event_CaretMoved(plugin), new SC_Event_EveryNSeconds(plugin), new SC_Event_FileMenu(plugin), new SC_Event_FolderMenu(plugin), new SC_Event_EditorMenu(plugin), new SC_Event_FileContentModified(plugin), new SC_Event_FileCreated(plugin), new SC_Event_FileDeleted(plugin), new SC_Event_FileMoved(plugin), new SC_Event_FileRenamed(plugin), new SC_Event_FolderCreated(plugin), new SC_Event_FolderDeleted(plugin), new SC_Event_FolderMoved(plugin), new SC_Event_FolderRenamed(plugin));
    }
    return eventList;
}
const eventList = [];

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * A wrapper for Obsidian's setIcon(), but with the difference that this one does not need a container as a parameter.
 */
function getIconHTML(icon_id) {
    if (!icon_id) {
        return "";
    }
    const icon_container = document.body.createEl("div"); // A temporary element, will be deleted soon. Not nice to create a temporary element in the body, but I don't know any better way.
    obsidian.setIcon(icon_container, icon_id);
    const icon_html = icon_container.innerHTML;
    icon_container.remove();
    return icon_html;
}
const unsortedIconList = obsidian.getIconIds().map((iconId) => {
    const icon = {
        iconId: iconId,
        displayName: iconId,
    };
    if (iconId.match(/^lucide-/)) {
        // Remove the `lucide-` prefix from `displayValue`.
        icon.displayName = iconId.replace(/^lucide-/, '');
    }
    return icon;
});
const SORTED_ICON_LIST = unsortedIconList.sort((iconA, iconB) => {
    if (iconA.displayName === iconB.displayName) {
        return 0;
    }
    else if (iconA.displayName > iconB.displayName) {
        return 1;
    }
    else {
        return -1;
    }
});
/**
 * The SC plugin up to version 0.22.0 used old style icon names. Those are still usable in Obsidian, but from SC 0.23.0 onwards the plugin migrates the icon names so that they can be found from the list returned by Obsidian's `getIconIds()`.
 *
 * Some icon names have only changed so that their name is prefixed with `lucide-`, but there are a few whose names have changed completely, e.g. `any-key` --> `lucide-plus-circle`.
 *
 * The following script was used to compile the migration list. You'll have to dig `AUGMENTED_ICON_LIST` from the history of this Git repository, as it was removed by the same commit that created this migration.
 * ```TypeScript
 *         const iconComparison: string[] = [];
 *         for (const oldIcon of AUGMENTED_ICON_LIST) {
 *             if (getIconIds().contains(oldIcon)) {
 *                 iconComparison.push(oldIcon + " = OK");
 *             }
 *             else {
 *                 const oldIconHTML = getIconHTML(oldIcon);
 *                 let foundWithAnotherName = false;
 *                 for (const newIcon of getIconIds()) {
 *                     const newIconHTML = getIconHTML(newIcon);
 *                     if (newIconHTML === oldIconHTML) {
 *                         iconComparison.push(oldIcon + " = Found as " + newIcon);
 *                         foundWithAnotherName = true;
 *                         break;
 *                     }
 *                 }
 *                 if (!foundWithAnotherName) {
 *                     iconComparison.push(oldIcon + " = NOT FOUND!");
 *                 }
 *             }
 *         }
 * ```
 */
const ICON_MIGRATIONS = {
    /* These icons do not need to be renamed, as they exist in the list returned by `getIconIds()`:
    bracket-glyph, broken-link, check-small, dice, heading-glyph, help, left-arrow, link, link-glyph, lucide-clock, lucide-cloud,
    lucide-cross, lucide-folder, lucide-info, lucide-languages, lucide-link, lucide-pencil, lucide-pin, lucide-search, lucide-star,
    lucide-trash, open-vault, paused, right-arrow, right-triangle, sheets-in-box, star-list, stretch-horizontal, stretch-vertical,
    sync-small, uppercase-lowercase-a, vault
    */
    // Icon names that were contained in SC 0.22.0 icon list, but that never worked. Disable any occurrences of these.
    'file-x-2': null,
    'link-2-off': null,
    'logo-crystal': null,
    'sortAsc': null,
    'sortDesc': null,
    // Icons that are present in the `getIconIds()` list with another name:
    'activity': 'lucide-activity',
    'airplay': 'lucide-airplay',
    'alarm-check': 'lucide-alarm-check',
    'alarm-clock': 'lucide-alarm-clock',
    'alarm-clock-off': 'lucide-alarm-clock-off',
    'alarm-minus': 'lucide-alarm-minus',
    'alarm-plus': 'lucide-alarm-plus',
    'album': 'lucide-album',
    'alert-circle': 'lucide-alert-circle',
    'alert-octagon': 'lucide-alert-octagon',
    'alert-triangle': 'lucide-alert-triangle',
    'align-center': 'lucide-align-center',
    'align-center-horizontal': 'lucide-align-center-horizontal',
    'align-center-vertical': 'lucide-align-center-vertical',
    'align-end-horizontal': 'lucide-align-end-horizontal',
    'align-end-vertical': 'lucide-align-end-vertical',
    'align-horizontal-distribute-center': 'lucide-align-horizontal-distribute-center',
    'align-horizontal-distribute-end': 'lucide-align-horizontal-distribute-end',
    'align-horizontal-distribute-start': 'lucide-align-horizontal-distribute-start',
    'align-horizontal-justify-center': 'lucide-align-horizontal-justify-center',
    'align-horizontal-justify-end': 'lucide-align-horizontal-justify-end',
    'align-horizontal-justify-start': 'lucide-align-horizontal-justify-start',
    'align-horizontal-space-around': 'lucide-align-horizontal-space-around',
    'align-horizontal-space-between': 'lucide-align-horizontal-space-between',
    'align-justify': 'lucide-align-justify',
    'align-left': 'lucide-align-left',
    'align-right': 'lucide-align-right',
    'align-start-horizontal': 'lucide-align-start-horizontal',
    'align-start-vertical': 'lucide-align-start-vertical',
    'align-vertical-distribute-center': 'lucide-align-vertical-distribute-center',
    'align-vertical-distribute-end': 'lucide-align-vertical-distribute-end',
    'align-vertical-distribute-start': 'lucide-align-vertical-distribute-start',
    'align-vertical-justify-center': 'lucide-align-vertical-justify-center',
    'align-vertical-justify-end': 'lucide-align-vertical-justify-end',
    'align-vertical-justify-start': 'lucide-align-vertical-justify-start',
    'align-vertical-space-around': 'lucide-align-vertical-space-around',
    'align-vertical-space-between': 'lucide-align-vertical-space-between',
    'anchor': 'lucide-anchor',
    'any-key': 'lucide-plus-circle',
    'aperture': 'lucide-aperture',
    'archive': 'lucide-archive',
    'arrow-big-down': 'lucide-arrow-big-down',
    'arrow-big-left': 'lucide-arrow-big-left',
    'arrow-big-right': 'lucide-arrow-big-right',
    'arrow-big-up': 'lucide-arrow-big-up',
    'arrow-down': 'lucide-arrow-down',
    'arrow-down-circle': 'lucide-arrow-down-circle',
    'arrow-down-left': 'lucide-arrow-down-left',
    'arrow-down-right': 'lucide-arrow-down-right',
    'arrow-left': 'lucide-arrow-left',
    'arrow-left-circle': 'lucide-arrow-left-circle',
    'arrow-left-right': 'lucide-arrow-left-right',
    'arrow-right': 'lucide-arrow-right',
    'arrow-right-circle': 'lucide-arrow-right-circle',
    'arrow-up': 'lucide-arrow-up',
    'arrow-up-circle': 'lucide-arrow-up-circle',
    'arrow-up-left': 'lucide-arrow-up-left',
    'arrow-up-right': 'lucide-arrow-up-right',
    'asterisk': 'lucide-asterisk',
    'at-sign': 'lucide-at-sign',
    'audio-file': 'lucide-file-audio',
    'award': 'lucide-award',
    'axe': 'lucide-axe',
    'banknote': 'lucide-banknote',
    'bar-chart': 'lucide-bar-chart',
    'bar-chart-2': 'lucide-bar-chart-2',
    'baseline': 'lucide-baseline',
    'battery': 'lucide-battery',
    'battery-charging': 'lucide-battery-charging',
    'battery-full': 'lucide-battery-full',
    'battery-low': 'lucide-battery-low',
    'battery-medium': 'lucide-battery-medium',
    'beaker': 'lucide-beaker',
    'bell': 'lucide-bell',
    'bell-minus': 'lucide-bell-minus',
    'bell-off': 'lucide-bell-off',
    'bell-plus': 'lucide-bell-plus',
    'bell-ring': 'lucide-bell-ring',
    'bike': 'lucide-bike',
    'binary': 'lucide-binary',
    'bitcoin': 'lucide-bitcoin',
    'blocks': 'lucide-layout-list',
    'bluetooth': 'lucide-bluetooth',
    'bluetooth-connected': 'lucide-bluetooth-connected',
    'bluetooth-off': 'lucide-bluetooth-off',
    'bluetooth-searching': 'lucide-bluetooth-searching',
    'bold': 'lucide-bold',
    'bold-glyph': 'lucide-bold',
    'book': 'lucide-book',
    'book-open': 'lucide-book-open',
    'bookmark': 'lucide-bookmark',
    'bookmark-minus': 'lucide-bookmark-minus',
    'bookmark-plus': 'lucide-bookmark-plus',
    'bot': 'lucide-bot',
    'box': 'lucide-box',
    'box-select': 'lucide-box-select',
    'briefcase': 'lucide-briefcase',
    'brush': 'lucide-brush',
    'bug': 'lucide-bug',
    'building': 'lucide-building',
    'building-2': 'lucide-building-2',
    'bullet-list': 'lucide-list',
    'bullet-list-glyph': 'lucide-list',
    'bus': 'lucide-bus',
    'calculator': 'lucide-calculator',
    'calendar': 'lucide-calendar',
    'calendar-with-checkmark': 'lucide-calendar-check',
    'camera': 'lucide-camera',
    'camera-off': 'lucide-camera-off',
    'car': 'lucide-car',
    'carrot': 'lucide-carrot',
    'cast': 'lucide-cast',
    'check': 'lucide-check',
    'check-circle': 'lucide-check-circle',
    'check-circle-2': 'lucide-check-circle-2',
    'check-in-circle': 'lucide-check-circle-2',
    'check-square': 'lucide-check-square',
    'checkbox-glyph': 'lucide-check-square',
    'checkmark': 'lucide-check',
    'chevron-down': 'lucide-chevron-down',
    'chevron-first': 'lucide-chevron-first',
    'chevron-last': 'lucide-chevron-last',
    'chevron-left': 'lucide-chevron-left',
    'chevron-right': 'lucide-chevron-right',
    'chevron-up': 'lucide-chevron-up',
    'chevrons-down': 'lucide-chevrons-down',
    'chevrons-down-up': 'lucide-chevrons-down-up',
    'chevrons-left': 'lucide-chevrons-left',
    'chevrons-right': 'lucide-chevrons-right',
    'chevrons-up': 'lucide-chevrons-up',
    'chevrons-up-down': 'lucide-chevrons-up-down',
    'chrome': 'lucide-chrome',
    'circle': 'lucide-circle',
    'circle-slashed': 'lucide-circle-slashed',
    'clipboard': 'lucide-clipboard',
    'clipboard-check': 'lucide-clipboard-check',
    'clipboard-copy': 'lucide-clipboard-copy',
    'clipboard-list': 'lucide-clipboard-list',
    'clipboard-x': 'lucide-clipboard-x',
    'clock': 'lucide-clock',
    'clock-1': 'lucide-clock-1',
    'clock-10': 'lucide-clock-10',
    'clock-11': 'lucide-clock-11',
    'clock-12': 'lucide-clock-12',
    'clock-2': 'lucide-clock-2',
    'clock-3': 'lucide-clock-3',
    'clock-4': 'lucide-clock-4',
    'clock-5': 'lucide-clock-5',
    'clock-6': 'lucide-clock-6',
    'clock-7': 'lucide-clock-7',
    'clock-8': 'lucide-clock-8',
    'clock-9': 'lucide-clock-9',
    'cloud': 'lucide-cloud',
    'cloud-drizzle': 'lucide-cloud-drizzle',
    'cloud-fog': 'lucide-cloud-fog',
    'cloud-hail': 'lucide-cloud-hail',
    'cloud-lightning': 'lucide-cloud-lightning',
    'cloud-moon': 'lucide-cloud-moon',
    'cloud-off': 'lucide-cloud-off',
    'cloud-rain': 'lucide-cloud-rain',
    'cloud-rain-wind': 'lucide-cloud-rain-wind',
    'cloud-snow': 'lucide-cloud-snow',
    'cloud-sun': 'lucide-cloud-sun',
    'cloudy': 'lucide-cloudy',
    'clover': 'lucide-clover',
    'code': 'lucide-code',
    'code-2': 'lucide-code-2',
    'code-glyph': 'lucide-code-2',
    'codepen': 'lucide-codepen',
    'codesandbox': 'lucide-codesandbox',
    'coffee': 'lucide-coffee',
    'coins': 'lucide-coins',
    'columns': 'lucide-columns',
    'command': 'lucide-command',
    'compass': 'lucide-compass',
    'contact': 'lucide-contact',
    'contrast': 'lucide-contrast',
    'cookie': 'lucide-cookie',
    'copy': 'lucide-copy',
    'copyleft': 'lucide-copyleft',
    'copyright': 'lucide-copyright',
    'corner-down-left': 'lucide-corner-down-left',
    'corner-down-right': 'lucide-corner-down-right',
    'corner-left-down': 'lucide-corner-left-down',
    'corner-left-up': 'lucide-corner-left-up',
    'corner-right-down': 'lucide-corner-right-down',
    'corner-right-up': 'lucide-corner-right-up',
    'corner-up-left': 'lucide-corner-up-left',
    'corner-up-right': 'lucide-corner-up-right',
    'cpu': 'lucide-cpu',
    'create-new': 'lucide-edit',
    'credit-card': 'lucide-credit-card',
    'crop': 'lucide-crop',
    'cross': 'lucide-x',
    'cross-in-box': 'lucide-x-square',
    'crossed-star': 'lucide-star-off',
    'crosshair': 'lucide-crosshair',
    'crown': 'lucide-crown',
    'currency': 'lucide-currency',
    'database': 'lucide-database',
    'delete': 'lucide-delete',
    'dice-1': 'lucide-dice-1',
    'dice-2': 'lucide-dice-2',
    'dice-3': 'lucide-dice-3',
    'dice-4': 'lucide-dice-4',
    'dice-5': 'lucide-dice-5',
    'dice-6': 'lucide-dice-6',
    'disc': 'lucide-disc',
    'divide': 'lucide-divide',
    'divide-circle': 'lucide-divide-circle',
    'divide-square': 'lucide-divide-square',
    'document': 'lucide-file',
    'documents': 'lucide-files',
    'dollar-sign': 'lucide-dollar-sign',
    'dot-network': 'lucide-git-fork',
    'double-down-arrow-glyph': 'lucide-chevrons-down',
    'double-up-arrow-glyph': 'lucide-chevrons-up',
    'down-arrow-with-tail': 'lucide-arrow-down',
    'down-chevron-glyph': 'lucide-chevron-down',
    'download': 'lucide-download',
    'download-cloud': 'lucide-download-cloud',
    'dribbble': 'lucide-dribbble',
    'droplet': 'lucide-droplet',
    'droplets': 'lucide-droplets',
    'drumstick': 'lucide-drumstick',
    'edit': 'lucide-edit',
    'edit-2': 'lucide-edit-2',
    'edit-3': 'lucide-edit-3',
    'egg': 'lucide-egg',
    'enter': 'lucide-log-in',
    'equal': 'lucide-equal',
    'equal-not': 'lucide-equal-not',
    'eraser': 'lucide-eraser',
    'euro': 'lucide-euro',
    'exit-fullscreen': 'lucide-minimize',
    'expand': 'lucide-expand',
    'expand-vertically': 'lucide-move-vertical',
    'external-link': 'lucide-external-link',
    'eye': 'lucide-eye',
    'eye-off': 'lucide-eye-off',
    'facebook': 'lucide-facebook',
    'fast-forward': 'lucide-fast-forward',
    'feather': 'lucide-feather',
    'figma': 'lucide-figma',
    'file': 'lucide-file',
    'file-check': 'lucide-file-check',
    'file-check-2': 'lucide-file-check-2',
    'file-code': 'lucide-file-code',
    'file-digit': 'lucide-file-digit',
    'file-input': 'lucide-file-input',
    'file-minus': 'lucide-file-minus',
    'file-minus-2': 'lucide-file-minus-2',
    'file-output': 'lucide-file-output',
    'file-plus': 'lucide-file-plus',
    'file-plus-2': 'lucide-file-plus-2',
    'file-search': 'lucide-file-search',
    'file-text': 'lucide-file-text',
    'file-x': 'lucide-file-x',
    'files': 'lucide-files',
    'filled-pin': 'lucide-pin',
    'film': 'lucide-film',
    'filter': 'lucide-filter',
    'flag': 'lucide-flag',
    'flag-off': 'lucide-flag-off',
    'flag-triangle-left': 'lucide-flag-triangle-left',
    'flag-triangle-right': 'lucide-flag-triangle-right',
    'flame': 'lucide-flame',
    'flashlight': 'lucide-flashlight',
    'flashlight-off': 'lucide-flashlight-off',
    'flask-conical': 'lucide-flask-conical',
    'flask-round': 'lucide-flask-round',
    'folder': 'lucide-folder-open',
    'folder-minus': 'lucide-folder-minus',
    'folder-open': 'lucide-folder-open',
    'folder-plus': 'lucide-folder-plus',
    'form-input': 'lucide-form-input',
    'forward': 'lucide-forward',
    'forward-arrow': 'lucide-forward',
    'frame': 'lucide-frame',
    'framer': 'lucide-framer',
    'frown': 'lucide-frown',
    'fullscreen': 'lucide-maximize',
    'function-square': 'lucide-function-square',
    'gamepad': 'lucide-gamepad',
    'gamepad-2': 'lucide-gamepad-2',
    'gauge': 'lucide-gauge',
    'gavel': 'lucide-gavel',
    'gear': 'lucide-settings',
    'gem': 'lucide-gem',
    'ghost': 'lucide-ghost',
    'gift': 'lucide-gift',
    'git-branch': 'lucide-git-branch',
    'git-branch-plus': 'lucide-git-branch-plus',
    'git-commit': 'lucide-git-commit',
    'git-fork': 'lucide-git-fork',
    'git-merge': 'lucide-git-merge',
    'git-pull-request': 'lucide-git-pull-request',
    'github': 'lucide-github',
    'gitlab': 'lucide-gitlab',
    'glasses': 'lucide-glasses',
    'globe': 'lucide-globe',
    'globe-2': 'lucide-globe-2',
    'go-to-file': 'lucide-file-input',
    'grab': 'lucide-grab',
    'graduation-cap': 'lucide-graduation-cap',
    'grid': 'lucide-grid',
    'grip-horizontal': 'lucide-grip-horizontal',
    'grip-vertical': 'lucide-grip-vertical',
    'hammer': 'lucide-hammer',
    'hand': 'lucide-hand',
    'hand-metal': 'lucide-hand-metal',
    'hard-drive': 'lucide-hard-drive',
    'hard-hat': 'lucide-hard-hat',
    'hash': 'lucide-hash',
    'hashtag': 'lucide-hash',
    'haze': 'lucide-haze',
    'headphones': 'lucide-headphones',
    'heart': 'lucide-heart',
    'help-circle': 'lucide-help-circle',
    'hexagon': 'lucide-hexagon',
    'highlight-glyph': 'lucide-highlighter',
    'highlighter': 'lucide-highlighter',
    'history': 'lucide-history',
    'home': 'lucide-home',
    'horizontal-split': 'lucide-separator-horizontal',
    'image': 'lucide-image',
    'image-file': 'lucide-image',
    'image-glyph': 'lucide-paperclip',
    'image-minus': 'lucide-image-minus',
    'image-off': 'lucide-image-off',
    'image-plus': 'lucide-image-plus',
    'import': 'lucide-import',
    'inbox': 'lucide-inbox',
    'indent': 'lucide-indent',
    'indent-glyph': 'lucide-indent',
    'indian-rupee': 'lucide-indian-rupee',
    'infinity': 'lucide-infinity',
    'info': 'lucide-info',
    'inspect': 'lucide-inspect',
    'instagram': 'lucide-instagram',
    'install': 'lucide-download-cloud',
    'italic': 'lucide-italic',
    'italic-glyph': 'lucide-italic',
    'japanese-yen': 'lucide-japanese-yen',
    'key': 'lucide-key',
    'keyboard': 'lucide-keyboard',
    'keyboard-glyph': 'lucide-keyboard',
    'landmark': 'lucide-landmark',
    'languages': 'lucide-languages',
    'laptop': 'lucide-laptop',
    'laptop-2': 'lucide-laptop-2',
    'lasso': 'lucide-lasso',
    'lasso-select': 'lucide-lasso-select',
    'layers': 'lucide-layers',
    'layout': 'lucide-layout',
    'layout-dashboard': 'lucide-layout-dashboard',
    'layout-grid': 'lucide-layout-grid',
    'layout-list': 'lucide-layout-list',
    'layout-template': 'lucide-layout-template',
    'left-arrow-with-tail': 'lucide-arrow-left',
    'left-chevron-glyph': 'lucide-chevron-left',
    'library': 'lucide-library',
    'life-buoy': 'lucide-life-buoy',
    'lightbulb': 'lucide-lightbulb',
    'lightbulb-off': 'lucide-lightbulb-off',
    'lines-of-text': 'lucide-align-left',
    'link-2': 'lucide-link-2',
    'linkedin': 'lucide-linkedin',
    'list': 'lucide-list',
    'list-checks': 'lucide-list-checks',
    'list-minus': 'lucide-list-minus',
    'list-ordered': 'lucide-list-ordered',
    'list-plus': 'lucide-list-plus',
    'list-x': 'lucide-list-x',
    'loader': 'lucide-loader',
    'loader-2': 'lucide-loader-2',
    'locate': 'lucide-locate',
    'locate-fixed': 'lucide-locate-fixed',
    'locate-off': 'lucide-locate-off',
    'lock': 'lucide-lock',
    'log-in': 'lucide-log-in',
    'log-out': 'lucide-log-out',
    'magnifying-glass': 'lucide-search',
    'mail': 'lucide-mail',
    'map': 'lucide-map',
    'map-pin': 'lucide-map-pin',
    'maximize': 'lucide-maximize',
    'maximize-2': 'lucide-maximize-2',
    'megaphone': 'lucide-megaphone',
    'meh': 'lucide-meh',
    'menu': 'lucide-menu',
    'message-circle': 'lucide-message-circle',
    'message-square': 'lucide-message-square',
    'mic': 'lucide-mic',
    'mic-off': 'lucide-mic-off',
    'microphone': 'lucide-mic',
    'microphone-filled': 'lucide-mic',
    'minimize': 'lucide-minimize',
    'minimize-2': 'lucide-minimize-2',
    'minus': 'lucide-minus',
    'minus-circle': 'lucide-minus-circle',
    'minus-square': 'lucide-minus-square',
    'minus-with-circle': 'lucide-minus-circle',
    'monitor': 'lucide-monitor',
    'monitor-off': 'lucide-monitor-off',
    'monitor-speaker': 'lucide-monitor-speaker',
    'moon': 'lucide-moon',
    'more-horizontal': 'lucide-more-horizontal',
    'more-vertical': 'lucide-more-vertical',
    'mountain': 'lucide-mountain',
    'mountain-snow': 'lucide-mountain-snow',
    'mouse': 'lucide-mouse',
    'mouse-pointer': 'lucide-mouse-pointer',
    'mouse-pointer-2': 'lucide-mouse-pointer-2',
    'mouse-pointer-click': 'lucide-mouse-pointer-click',
    'move': 'lucide-move',
    'move-diagonal': 'lucide-move-diagonal',
    'move-diagonal-2': 'lucide-move-diagonal-2',
    'move-horizontal': 'lucide-move-horizontal',
    'move-vertical': 'lucide-move-vertical',
    'music': 'lucide-music',
    'navigation': 'lucide-navigation',
    'navigation-2': 'lucide-navigation-2',
    'network': 'lucide-network',
    'note-glyph': 'lucide-sticky-note',
    'number-list-glyph': 'lucide-list-ordered',
    'octagon': 'lucide-octagon',
    'option': 'lucide-option',
    'outdent': 'lucide-outdent',
    'package': 'lucide-package',
    'package-check': 'lucide-package-check',
    'package-minus': 'lucide-package-minus',
    'package-plus': 'lucide-package-plus',
    'package-search': 'lucide-package-search',
    'package-x': 'lucide-package-x',
    'palette': 'lucide-palette',
    'palmtree': 'lucide-palmtree',
    'pane-layout': 'lucide-layout',
    'paper-plane': 'lucide-send',
    'paperclip': 'lucide-paperclip',
    'pause': 'lucide-pause',
    'pause-circle': 'lucide-pause-circle',
    'pause-octagon': 'lucide-pause-octagon',
    'pdf-file': 'lucide-file-text',
    'pen-tool': 'lucide-pen-tool',
    'pencil': 'lucide-edit-3',
    'percent': 'lucide-percent',
    'percent-sign-glyph': 'lucide-percent',
    'person-standing': 'lucide-person-standing',
    'phone': 'lucide-phone',
    'phone-call': 'lucide-phone-call',
    'phone-forwarded': 'lucide-phone-forwarded',
    'phone-incoming': 'lucide-phone-incoming',
    'phone-missed': 'lucide-phone-missed',
    'phone-off': 'lucide-phone-off',
    'phone-outgoing': 'lucide-phone-outgoing',
    'pie-chart': 'lucide-pie-chart',
    'piggy-bank': 'lucide-piggy-bank',
    'pin': 'lucide-pin',
    'pipette': 'lucide-pipette',
    'plane': 'lucide-plane',
    'play': 'lucide-play',
    'play-circle': 'lucide-play-circle',
    'plug-zap': 'lucide-plug-zap',
    'plus': 'lucide-plus',
    'plus-circle': 'lucide-plus-circle',
    'plus-square': 'lucide-plus-square',
    'plus-with-circle': 'lucide-plus-circle',
    'pocket': 'lucide-pocket',
    'podcast': 'lucide-podcast',
    'pointer': 'lucide-pointer',
    'popup-open': 'lucide-arrow-up-right',
    'pound-sterling': 'lucide-pound-sterling',
    'power': 'lucide-power',
    'power-off': 'lucide-power-off',
    'presentation': 'lucide-monitor',
    'price-tag-glyph': 'lucide-tag',
    'printer': 'lucide-printer',
    'qr-code': 'lucide-qr-code',
    'quote': 'lucide-quote',
    'quote-glyph': 'lucide-quote',
    'radio': 'lucide-radio',
    'radio-receiver': 'lucide-radio-receiver',
    'redo': 'lucide-redo',
    'redo-glyph': 'lucide-redo-2',
    'refresh-ccw': 'lucide-refresh-ccw',
    'refresh-cw': 'lucide-refresh-cw',
    'regex': 'lucide-regex',
    'repeat': 'lucide-repeat',
    'repeat-1': 'lucide-repeat-1',
    'reply': 'lucide-reply',
    'reply-all': 'lucide-reply-all',
    'reset': 'lucide-rotate-ccw',
    'rewind': 'lucide-rewind',
    'right-arrow-with-tail': 'lucide-arrow-right',
    'right-chevron-glyph': 'lucide-chevron-right',
    'rocket': 'lucide-rocket',
    'rocking-chair': 'lucide-rocking-chair',
    'rotate-ccw': 'lucide-rotate-ccw',
    'rotate-cw': 'lucide-rotate-cw',
    'rss': 'lucide-rss',
    'ruler': 'lucide-ruler',
    'run-command': 'lucide-terminal',
    'russian-ruble': 'lucide-russian-ruble',
    'save': 'lucide-save',
    'scale': 'lucide-scale',
    'scan': 'lucide-scan',
    'scan-line': 'lucide-scan-line',
    'scissors': 'lucide-scissors',
    'screen-share': 'lucide-screen-share',
    'screen-share-off': 'lucide-screen-share-off',
    'search': 'lucide-search',
    'send': 'lucide-send',
    'separator-horizontal': 'lucide-separator-horizontal',
    'separator-vertical': 'lucide-separator-vertical',
    'server': 'lucide-server',
    'server-crash': 'lucide-server-crash',
    'server-off': 'lucide-server-off',
    'settings': 'lucide-settings',
    'settings-2': 'lucide-settings-2',
    'share': 'lucide-share',
    'share-2': 'lucide-share-2',
    'sheet': 'lucide-sheet',
    'shield': 'lucide-shield',
    'shield-alert': 'lucide-shield-alert',
    'shield-check': 'lucide-shield-check',
    'shield-close': 'lucide-shield-close',
    'shield-off': 'lucide-shield-off',
    'shirt': 'lucide-shirt',
    'shopping-bag': 'lucide-shopping-bag',
    'shopping-cart': 'lucide-shopping-cart',
    'shovel': 'lucide-shovel',
    'shrink': 'lucide-shrink',
    'shuffle': 'lucide-shuffle',
    'sidebar': 'lucide-sidebar',
    'sidebar-close': 'lucide-sidebar-close',
    'sidebar-open': 'lucide-sidebar-open',
    'sigma': 'lucide-sigma',
    'signal': 'lucide-signal',
    'signal-high': 'lucide-signal-high',
    'signal-low': 'lucide-signal-low',
    'signal-medium': 'lucide-signal-medium',
    'signal-zero': 'lucide-signal-zero',
    'skip-back': 'lucide-skip-back',
    'skip-forward': 'lucide-skip-forward',
    'skull': 'lucide-skull',
    'slack': 'lucide-slack',
    'slash': 'lucide-slash',
    'sliders': 'lucide-sliders',
    'smartphone': 'lucide-smartphone',
    'smartphone-charging': 'lucide-smartphone-charging',
    'smile': 'lucide-smile',
    'snowflake': 'lucide-snowflake',
    'sort-asc': 'lucide-sort-asc',
    'sort-desc': 'lucide-sort-desc',
    'speaker': 'lucide-speaker',
    'sprout': 'lucide-sprout',
    'square': 'lucide-square',
    'stacked-levels': 'lucide-folder-tree',
    'star': 'lucide-star',
    'star-half': 'lucide-star-half',
    'stop-circle': 'lucide-stop-circle',
    'strikethrough': 'lucide-strikethrough',
    'strikethrough-glyph': 'lucide-strikethrough',
    'subscript': 'lucide-subscript',
    'sun': 'lucide-sun',
    'sunrise': 'lucide-sunrise',
    'sunset': 'lucide-sunset',
    'superscript': 'lucide-superscript',
    'swiss-franc': 'lucide-swiss-franc',
    'switch': 'lucide-repeat',
    'switch-camera': 'lucide-switch-camera',
    'sync': 'lucide-refresh-cw',
    'table': 'lucide-table',
    'tablet': 'lucide-tablet',
    'tag': 'lucide-tag',
    'tag-glyph': 'lucide-tag',
    'target': 'lucide-target',
    'tent': 'lucide-tent',
    'terminal': 'lucide-terminal',
    'terminal-square': 'lucide-terminal-square',
    'text-cursor': 'lucide-text-cursor',
    'text-cursor-input': 'lucide-text-cursor-input',
    'thermometer': 'lucide-thermometer',
    'thermometer-snowflake': 'lucide-thermometer-snowflake',
    'thermometer-sun': 'lucide-thermometer-sun',
    'three-horizontal-bars': 'lucide-menu',
    'thumbs-down': 'lucide-thumbs-down',
    'thumbs-up': 'lucide-thumbs-up',
    'ticket': 'lucide-ticket',
    'timer': 'lucide-timer',
    'timer-off': 'lucide-timer-off',
    'timer-reset': 'lucide-timer-reset',
    'toggle-left': 'lucide-toggle-left',
    'toggle-right': 'lucide-toggle-right',
    'tornado': 'lucide-tornado',
    'trash': 'lucide-trash-2',
    'trash-2': 'lucide-trash-2',
    'trello': 'lucide-trello',
    'trending-down': 'lucide-trending-down',
    'trending-up': 'lucide-trending-up',
    'triangle': 'lucide-triangle',
    'truck': 'lucide-truck',
    'tv': 'lucide-tv',
    'tv-2': 'lucide-tv-2',
    'twitch': 'lucide-twitch',
    'twitter': 'lucide-twitter',
    'type': 'lucide-type',
    'umbrella': 'lucide-umbrella',
    'underline': 'lucide-underline',
    'undo': 'lucide-undo',
    'undo-glyph': 'lucide-undo-2',
    'unindent-glyph': 'lucide-outdent',
    'unlink': 'lucide-unlink',
    'unlink-2': 'lucide-unlink-2',
    'unlock': 'lucide-unlock',
    'up-and-down-arrows': 'lucide-move-vertical',
    'up-arrow-with-tail': 'lucide-arrow-up',
    'up-chevron-glyph': 'lucide-chevron-up',
    'upload': 'lucide-upload',
    'upload-cloud': 'lucide-upload-cloud',
    'user': 'lucide-user',
    'user-check': 'lucide-user-check',
    'user-minus': 'lucide-user-minus',
    'user-plus': 'lucide-user-plus',
    'user-x': 'lucide-user-x',
    'users': 'lucide-users',
    'verified': 'lucide-verified',
    'vertical-split': 'lucide-separator-vertical',
    'vertical-three-dots': 'lucide-more-vertical',
    'vibrate': 'lucide-vibrate',
    'video': 'lucide-video',
    'video-off': 'lucide-video-off',
    'view': 'lucide-view',
    'voicemail': 'lucide-voicemail',
    'volume': 'lucide-volume',
    'volume-1': 'lucide-volume-1',
    'volume-2': 'lucide-volume-2',
    'volume-x': 'lucide-volume-x',
    'wallet': 'lucide-wallet',
    'wand': 'lucide-wand-2',
    'watch': 'lucide-watch',
    'waves': 'lucide-waves',
    'webcam': 'lucide-webcam',
    'wifi': 'lucide-wifi',
    'wifi-off': 'lucide-wifi-off',
    'wind': 'lucide-wind',
    'wrap-text': 'lucide-wrap-text',
    'wrench': 'lucide-wrench',
    'wrench-screwdriver-glyph': 'lucide-wrench',
    'x': 'lucide-x',
    'x-circle': 'lucide-x-circle',
    'x-octagon': 'lucide-x-octagon',
    'x-square': 'lucide-x-square',
    'youtube': 'lucide-youtube',
    'zap': 'lucide-zap',
    'zap-off': 'lucide-zap-off',
    'zoom-in': 'lucide-zoom-in',
    'zoom-out': 'lucide-zoom-out',
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Escaper {
    constructor(raw_value) {
        this.raw_value = raw_value;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * Prefixes all characters that are not letters, numbers or underscores with a prefix character that can be defined by child classes.
 */
class AllSpecialCharactersEscaper extends Escaper {
    escape() {
        return this.raw_value.replace(/[^\w\d]/gu, (special_character) => {
            // Do the replacing in a function in order to avoid a possible $ character to be interpreted by JavaScript to interact with the regex.
            // More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter (referenced 2021-11-02.
            return this.prefix + special_character;
        });
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class ShEscaper extends AllSpecialCharactersEscaper {
    constructor() {
        super(...arguments);
        this.prefix = "\\"; // In *sh, escaping should use a backslash, e.g. "Hello, world!" becomes \"Hello\,\ world\!\"
    }
    escape() {
        return this.replace_newlines(super.escape());
    }
    /**
     * Converts escaped newline characters to a form that the Bourne family shells will interpret as literal newlines,
     * not as ignorable characters.
     *
     * @param escaped_value
     * @private
     */
    replace_newlines(escaped_value) {
        return escaped_value
            .replaceAll(this.prefix + "\r", this.prefix + this.prefix + "r") // Replace a real linefeed with a literal "\\r".
            .replaceAll(this.prefix + "\n", this.prefix + this.prefix + "n") // Replace a real newline with a literal "\\n".
        ;
    }
}

var autocomplete = createCommonjsModule(function (module, exports) {
(function (global, factory) {
  module.exports = factory() ;
}(commonjsGlobal, (function () {
  /*
   * https://github.com/kraaden/autocomplete
   * Copyright (c) 2016 Denys Krasnoshchok
   * MIT License
   */
  function autocomplete(settings) {
      // just an alias to minimize JS file size
      var doc = document;
      var container = settings.container || doc.createElement("div");
      var containerStyle = container.style;
      var userAgent = navigator.userAgent;
      var mobileFirefox = ~userAgent.indexOf("Firefox") && ~userAgent.indexOf("Mobile");
      var debounceWaitMs = settings.debounceWaitMs || 0;
      var preventSubmit = settings.preventSubmit || false;
      var disableAutoSelect = settings.disableAutoSelect || false;
      // 'keyup' event will not be fired on Mobile Firefox, so we have to use 'input' event instead
      var keyUpEventName = mobileFirefox ? "input" : "keyup";
      var items = [];
      var inputValue = "";
      var minLen = 2;
      var showOnFocus = settings.showOnFocus;
      var selected;
      var keypressCounter = 0;
      var debounceTimer;
      if (settings.minLength !== undefined) {
          minLen = settings.minLength;
      }
      if (!settings.input) {
          throw new Error("input undefined");
      }
      var input = settings.input;
      container.className = "autocomplete " + (settings.className || "");
      // IOS implementation for fixed positioning has many bugs, so we will use absolute positioning
      containerStyle.position = "absolute";
      /**
       * Detach the container from DOM
       */
      function detach() {
          var parent = container.parentNode;
          if (parent) {
              parent.removeChild(container);
          }
      }
      /**
       * Clear debouncing timer if assigned
       */
      function clearDebounceTimer() {
          if (debounceTimer) {
              window.clearTimeout(debounceTimer);
          }
      }
      /**
       * Attach the container to DOM
       */
      function attach() {
          if (!container.parentNode) {
              doc.body.appendChild(container);
          }
      }
      /**
       * Check if container for autocomplete is displayed
       */
      function containerDisplayed() {
          return !!container.parentNode;
      }
      /**
       * Clear autocomplete state and hide container
       */
      function clear() {
          // prevent the update call if there are pending AJAX requests
          keypressCounter++;
          items = [];
          inputValue = "";
          selected = undefined;
          detach();
      }
      /**
       * Update autocomplete position
       */
      function updatePosition() {
          if (!containerDisplayed()) {
              return;
          }
          containerStyle.height = "auto";
          containerStyle.width = input.offsetWidth + "px";
          var maxHeight = 0;
          var inputRect;
          function calc() {
              var docEl = doc.documentElement;
              var clientTop = docEl.clientTop || doc.body.clientTop || 0;
              var clientLeft = docEl.clientLeft || doc.body.clientLeft || 0;
              var scrollTop = window.pageYOffset || docEl.scrollTop;
              var scrollLeft = window.pageXOffset || docEl.scrollLeft;
              inputRect = input.getBoundingClientRect();
              var top = inputRect.top + input.offsetHeight + scrollTop - clientTop;
              var left = inputRect.left + scrollLeft - clientLeft;
              containerStyle.top = top + "px";
              containerStyle.left = left + "px";
              maxHeight = window.innerHeight - (inputRect.top + input.offsetHeight);
              if (maxHeight < 0) {
                  maxHeight = 0;
              }
              containerStyle.top = top + "px";
              containerStyle.bottom = "";
              containerStyle.left = left + "px";
              containerStyle.maxHeight = maxHeight + "px";
          }
          // the calc method must be called twice, otherwise the calculation may be wrong on resize event (chrome browser)
          calc();
          calc();
          if (settings.customize && inputRect) {
              settings.customize(input, inputRect, container, maxHeight);
          }
      }
      /**
       * Redraw the autocomplete div element with suggestions
       */
      function update() {
          // delete all children from autocomplete DOM container
          while (container.firstChild) {
              container.removeChild(container.firstChild);
          }
          // function for rendering autocomplete suggestions
          var render = function (item, currentValue) {
              var itemElement = doc.createElement("div");
              itemElement.textContent = item.label || "";
              return itemElement;
          };
          if (settings.render) {
              render = settings.render;
          }
          // function to render autocomplete groups
          var renderGroup = function (groupName, currentValue) {
              var groupDiv = doc.createElement("div");
              groupDiv.textContent = groupName;
              return groupDiv;
          };
          if (settings.renderGroup) {
              renderGroup = settings.renderGroup;
          }
          var fragment = doc.createDocumentFragment();
          var prevGroup = "#9?$";
          items.forEach(function (item) {
              if (item.group && item.group !== prevGroup) {
                  prevGroup = item.group;
                  var groupDiv = renderGroup(item.group, inputValue);
                  if (groupDiv) {
                      groupDiv.className += " group";
                      fragment.appendChild(groupDiv);
                  }
              }
              var div = render(item, inputValue);
              if (div) {
                  div.addEventListener("click", function (ev) {
                      settings.onSelect(item, input);
                      clear();
                      ev.preventDefault();
                      ev.stopPropagation();
                  });
                  if (item === selected) {
                      div.className += " selected";
                  }
                  fragment.appendChild(div);
              }
          });
          container.appendChild(fragment);
          if (items.length < 1) {
              if (settings.emptyMsg) {
                  var empty = doc.createElement("div");
                  empty.className = "empty";
                  empty.textContent = settings.emptyMsg;
                  container.appendChild(empty);
              }
              else {
                  clear();
                  return;
              }
          }
          attach();
          updatePosition();
          updateScroll();
      }
      function updateIfDisplayed() {
          if (containerDisplayed()) {
              update();
          }
      }
      function resizeEventHandler() {
          updateIfDisplayed();
      }
      function scrollEventHandler(e) {
          if (e.target !== container) {
              updateIfDisplayed();
          }
          else {
              e.preventDefault();
          }
      }
      function keyupEventHandler(ev) {
          var keyCode = ev.which || ev.keyCode || 0;
          var ignore = settings.keysToIgnore || [38 /* Up */, 13 /* Enter */, 27 /* Esc */, 39 /* Right */, 37 /* Left */, 16 /* Shift */, 17 /* Ctrl */, 18 /* Alt */, 20 /* CapsLock */, 91 /* WindowsKey */, 9 /* Tab */];
          for (var _i = 0, ignore_1 = ignore; _i < ignore_1.length; _i++) {
              var key = ignore_1[_i];
              if (keyCode === key) {
                  return;
              }
          }
          if (keyCode >= 112 /* F1 */ && keyCode <= 123 /* F12 */ && !settings.keysToIgnore) {
              return;
          }
          // the down key is used to open autocomplete
          if (keyCode === 40 /* Down */ && containerDisplayed()) {
              return;
          }
          startFetch(0 /* Keyboard */);
      }
      /**
       * Automatically move scroll bar if selected item is not visible
       */
      function updateScroll() {
          var elements = container.getElementsByClassName("selected");
          if (elements.length > 0) {
              var element = elements[0];
              // make group visible
              var previous = element.previousElementSibling;
              if (previous && previous.className.indexOf("group") !== -1 && !previous.previousElementSibling) {
                  element = previous;
              }
              if (element.offsetTop < container.scrollTop) {
                  container.scrollTop = element.offsetTop;
              }
              else {
                  var selectBottom = element.offsetTop + element.offsetHeight;
                  var containerBottom = container.scrollTop + container.offsetHeight;
                  if (selectBottom > containerBottom) {
                      container.scrollTop += selectBottom - containerBottom;
                  }
              }
          }
      }
      /**
       * Select the previous item in suggestions
       */
      function selectPrev() {
          if (items.length < 1) {
              selected = undefined;
          }
          else {
              if (selected === items[0]) {
                  selected = items[items.length - 1];
              }
              else {
                  for (var i = items.length - 1; i > 0; i--) {
                      if (selected === items[i] || i === 1) {
                          selected = items[i - 1];
                          break;
                      }
                  }
              }
          }
      }
      /**
       * Select the next item in suggestions
       */
      function selectNext() {
          if (items.length < 1) {
              selected = undefined;
          }
          if (!selected || selected === items[items.length - 1]) {
              selected = items[0];
              return;
          }
          for (var i = 0; i < (items.length - 1); i++) {
              if (selected === items[i]) {
                  selected = items[i + 1];
                  break;
              }
          }
      }
      function keydownEventHandler(ev) {
          var keyCode = ev.which || ev.keyCode || 0;
          if (keyCode === 38 /* Up */ || keyCode === 40 /* Down */ || keyCode === 27 /* Esc */) {
              var containerIsDisplayed = containerDisplayed();
              if (keyCode === 27 /* Esc */) {
                  clear();
              }
              else {
                  if (!containerIsDisplayed || items.length < 1) {
                      return;
                  }
                  keyCode === 38 /* Up */
                      ? selectPrev()
                      : selectNext();
                  update();
              }
              ev.preventDefault();
              if (containerIsDisplayed) {
                  ev.stopPropagation();
              }
              return;
          }
          if (keyCode === 13 /* Enter */) {
              if (selected) {
                  if (preventSubmit) {
                      ev.preventDefault();
                  }
                  settings.onSelect(selected, input);
                  clear();
              }
          }
      }
      function focusEventHandler() {
          if (showOnFocus) {
              startFetch(1 /* Focus */);
          }
      }
      function startFetch(trigger) {
          // If multiple keys were pressed, before we get an update from server,
          // this may cause redrawing autocomplete multiple times after the last key was pressed.
          // To avoid this, the number of times keyboard was pressed will be saved and checked before redraw.
          var savedKeypressCounter = ++keypressCounter;
          var inputText = input.value;
          var cursorPos = input.selectionStart || 0;
          if (inputText.length >= minLen || trigger === 1 /* Focus */) {
              clearDebounceTimer();
              debounceTimer = window.setTimeout(function () {
                  settings.fetch(inputText, function (elements) {
                      if (keypressCounter === savedKeypressCounter && elements) {
                          items = elements;
                          inputValue = inputText;
                          selected = (items.length < 1 || disableAutoSelect) ? undefined : items[0];
                          update();
                      }
                  }, trigger, cursorPos);
              }, trigger === 0 /* Keyboard */ ? debounceWaitMs : 0);
          }
          else {
              clear();
          }
      }
      function blurEventHandler() {
          // we need to delay clear, because when we click on an item, blur will be called before click and remove items from DOM
          setTimeout(function () {
              if (doc.activeElement !== input) {
                  clear();
              }
          }, 200);
      }
      /**
       * Fixes #26: on long clicks focus will be lost and onSelect method will not be called
       */
      container.addEventListener("mousedown", function (evt) {
          evt.stopPropagation();
          evt.preventDefault();
      });
      /**
       * Fixes #30: autocomplete closes when scrollbar is clicked in IE
       * See: https://stackoverflow.com/a/9210267/13172349
       */
      container.addEventListener("focus", function () { return input.focus(); });
      /**
       * This function will remove DOM elements and clear event handlers
       */
      function destroy() {
          input.removeEventListener("focus", focusEventHandler);
          input.removeEventListener("keydown", keydownEventHandler);
          input.removeEventListener(keyUpEventName, keyupEventHandler);
          input.removeEventListener("blur", blurEventHandler);
          window.removeEventListener("resize", resizeEventHandler);
          doc.removeEventListener("scroll", scrollEventHandler, true);
          clearDebounceTimer();
          clear();
      }
      // setup event handlers
      input.addEventListener("keydown", keydownEventHandler);
      input.addEventListener(keyUpEventName, keyupEventHandler);
      input.addEventListener("blur", blurEventHandler);
      input.addEventListener("focus", focusEventHandler);
      window.addEventListener("resize", resizeEventHandler);
      doc.addEventListener("scroll", scrollEventHandler, true);
      return {
          destroy: destroy
      };
  }

  return autocomplete;

})));

});

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 *
 * @param plugin Used for getting a list of Variable autocomplete items.
 * @param input_element
 * @param call_on_completion A function that will be called when a user has selected a suggestion and performed the autocomplete action. onChange event will not be called, because it would trigger opening the autocomplete menu again, so that's why a separate callback is used.
 * @param extra_autocomplete_items
 */
function createAutocomplete(plugin, input_element, call_on_completion, extra_autocomplete_items = []) {
    const autocompleteMenu = autocomplete({
        input: input_element,
        fetch: (input_value_but_not_used, update) => {
            const autocomplete_items = merge_and_sort_autocomplete_items(getVariableAutocompleteItems(plugin), CustomAutocompleteItems, extra_autocomplete_items);
            const max_suggestions = 30;
            // Get the so far typed text - exclude everything that is on the right side of the caret.
            const caret_position = input_element.selectionStart;
            if (null === caret_position) {
                throw new Error("createAutocomplete(): fetch(): caret_position is null.");
            }
            const typed_text = input_element.value.slice(0, caret_position);
            const search_query = get_search_query(typed_text);
            if ("" === search_query.search_text) {
                // No suggestions for empty word.
                update([]);
            }
            else {
                // The word is not empty, so can suggest something.
                let matched_items = autocomplete_items.filter(item => item_match(item, search_query));
                sort_autocomplete_items(matched_items, search_query);
                matched_items = matched_items.slice(0, max_suggestions); // Limit to a reasonable amount of suggestions.
                update(matched_items);
            }
        },
        onSelect: (item) => {
            // A user has selected an item to be autocompleted
            // Get the item text and already typed text
            let supplement = item.value;
            let caret_position = input_element.selectionStart;
            if (null === caret_position) {
                throw new Error("createAutocomplete(): fetch(): caret_position is null.");
            }
            const typed_text = input_element.value.slice(0, caret_position);
            const search_query = get_search_query(typed_text);
            const search_text = search_query.search_text;
            // Special case: Check if }} happens to appear after the caret
            const after_caret = input_element.value.slice(caret_position, caret_position + 2);
            if ("}}" === after_caret) {
                // The replacing will happen in a {{variable}}.
                // Do not accidentally insert another }} pair.
                supplement = supplement.replace(/\}\}$/u, ""); // Only removes a trailing }} if there is one.
            }
            // Try to save part of the beginning, in case it seems like not being part of the search query.
            let replace_start = find_starting_position(search_text, supplement); // The length difference of typed_text and search_text will be added here below.
            if (false === replace_start) {
                // This should never happen, but if it does, do not replace anything, just insert.
                replace_start = caret_position;
            }
            else {
                // Adjust the position
                replace_start += typed_text.length - search_text.length;
            }
            // Choose a method for doing the inserting
            if (undefined !== document.execCommand) {
                // execCommand() is deprecated, but available.
                // Use it to do the insertion, because this way an undo history can be preserved.
                input_element.setSelectionRange(replace_start, caret_position); // First select the part that will be replaced, because execCommand() does not support defining positions. This adds a cumbersome selection step to the undo history, but at least undoing works.
                document.execCommand("insertText", false, supplement);
            }
            else {
                // execCommand() is not available anymore.
                // Use setRangeText() to do the insertion. It will clear undo history, but at least the insertion works.
                input_element.setRangeText(supplement, replace_start, caret_position);
            }
            // Move the caret to a logical continuation point
            caret_position = replace_start + supplement.length;
            if (supplement.match(/:\}\}$/u)) {
                // Place the caret after the colon, instead of after }}.
                caret_position -= 2;
            }
            input_element.setSelectionRange(caret_position, caret_position);
            // Call a hook
            call_on_completion(input_element.value);
        },
        render: (item) => {
            const div_element = document.createElement("div");
            div_element.createSpan({ text: item.value, attr: { class: "SC-autocomplete-value" } });
            if (item.help_text) {
                div_element.createSpan({ text: ": ", attr: { class: "SC-autocomplete-separator" } });
                div_element.createSpan({ attr: { class: "SC-autocomplete-help-text" } }).insertAdjacentHTML("beforeend", item.help_text);
            }
            // Documentation link
            const documentationLink = item.documentationLink;
            if (undefined !== documentationLink) {
                const documentationLinkElement = div_element.createEl("a", { attr: { "title": "Documentation: " + item.value } }); // Use "title" instead of "aria-label", because I don't know how to make "aria-label" show a tooltip box on a custom element.
                obsidian.setIcon(documentationLinkElement, "help");
                documentationLinkElement.addClass("SC-autocomplete-link-icon");
                documentationLinkElement.onClickEvent((event) => {
                    gotoURL(documentationLink);
                    // event.preventDefault(); Not needed, I guess.
                    event.stopImmediatePropagation(); // Do not close the autocomplete menu.
                });
            }
            return div_element;
        },
        minLength: 2,
        className: "SC-autocomplete",
        keysToIgnore: [38 /* Up */, 13 /* Enter */, 27 /* Esc */, 16 /* Shift */, 17 /* Ctrl */, 18 /* Alt */, 20 /* CapsLock */, 91 /* WindowsKey */, 9 /* Tab */],
        preventSubmit: true, // Prevents creating newlines in textareas when enter is pressed in the autocomplete menu.
    });
    // Make the plugin able to close the menu if the plugin is disabled (or restarted).
    plugin.registerAutocompleteMenu(autocompleteMenu);
}
function item_match(item, search_query) {
    const item_value = item.value.toLocaleLowerCase();
    const search_text = search_query.search_text.toLocaleLowerCase();
    // Match query type
    if (item.type !== search_query.search_type) {
        // If the query type is different, do not include this item.
        // This can happen e.g. if {{ is typed, and the item is not a variable, or {{! is typed, and the item is not an unescaped variable.
        return false;
    }
    // Match text
    let search_character;
    let search_position = 0;
    for (let search_character_index = 0; search_character_index < search_text.length; search_character_index++) {
        search_character = search_text[search_character_index];
        if (item_value.includes(search_character, search_position)) {
            // This character was found in item_value.
            search_position = item_value.indexOf(search_character, search_position) + 1;
        }
        else {
            // This character was not found.
            return false;
        }
    }
    return true;
}
function find_starting_position(typed_text, supplement) {
    typed_text = typed_text.toLocaleLowerCase();
    supplement = supplement.toLocaleLowerCase();
    for (let supplement_index = supplement.length; supplement_index >= 0; supplement_index--) {
        const partial_supplement = supplement.slice(0, supplement_index);
        if (typed_text.contains(partial_supplement)) {
            return typed_text.indexOf(partial_supplement);
        }
    }
    return false;
}
/**
 * Sorts in place, does not make a copy.
 * @param autocomplete_items
 * @param search_query
 */
function sort_autocomplete_items(autocomplete_items, search_query) {
    const search_text_excluding_curly_brackets = search_query.search_text.replace(/^{{!?/, "");
    function get_common_beginning_length(autocomplete_item, search_query) {
        const search_text = search_query.search_text.toLocaleLowerCase();
        const item_value = autocomplete_item.value.toLocaleLowerCase();
        const shortest_length = Math.min(search_text.length, item_value.length);
        for (let character_index = 0; character_index < shortest_length; character_index++) {
            const search_character = search_text[character_index];
            const item_character = item_value[character_index];
            if (search_character !== item_character) {
                // The common beginning has ended.
                return character_index;
            }
        }
        return shortest_length;
    }
    autocomplete_items.sort((a, b) => {
        const boost_a = Math.max(get_common_beginning_length(a, search_query), 1); // Boosts are used as multipliers,
        const boost_b = Math.max(get_common_beginning_length(b, search_query), 1); // so they cannot be zero.
        const a_length = a.value.length * boost_b; // boost_b worsens A (= makes it artificially "longer")
        const b_length = b.value.length * boost_a; // boost_a worsens B
        // Determine sorting method. If the search query is just a couple of characters, the matches would be quite vague, sorting by the matched items' length would not tell much, and the list would just look strangely ordered.
        if (search_text_excluding_curly_brackets.length < 2 || a_length === b_length) {
            // Sort alphabetically.
            return a.value > b.value ? 1 : -1;
        }
        else {
            // Sort by lengths. The shortest item is preferred. If an item has a long common beginning with the search query, boost the item up.
            return a_length - b_length;
        }
    });
}
const CustomAutocompleteItems = [];
function addCustomAutocompleteItems(custom_autocomplete_yaml) {
    // Ensure the content is not empty
    if (0 === custom_autocomplete_yaml.trim().length) {
        return "The content is empty.";
    }
    // Try to parse YAML syntax
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let yaml; // 'any' is defined in obsidian.d.ts for the return type of parseYaml(), so I made ESLint ignore it.
    try {
        yaml = obsidian.parseYaml(custom_autocomplete_yaml);
    }
    catch (error) {
        // A syntax error has appeared.
        return error.message;
    }
    if (null === yaml || typeof yaml !== "object") {
        return "Unable to parse the content due to unknown reason.";
    }
    // Iterate autocomplete item groups
    const group_names = Object.getOwnPropertyNames(yaml);
    const error_messages = [];
    group_names.forEach((group_name) => {
        const group_items = yaml[group_name];
        const group_item_values = Object.getOwnPropertyNames(group_items);
        // Iterate all autocomplete items in the group
        group_item_values.forEach((autocomplete_item_value) => {
            const autocomplete_item_label = group_items[autocomplete_item_value];
            if (typeof autocomplete_item_label !== "string") {
                error_messages.push("Autocomplete item '" + autocomplete_item_value + "' has an incorrect help text type: " + autocomplete_item_label + " is a " + typeof autocomplete_item_label + ", but it should be a string.");
                return;
            }
            // Determine a correct type for the item
            let type = "other";
            if (autocomplete_item_value.startsWith("{{")) {
                // This is a variable
                type = "normal-variable";
            }
            // The item is ok, add it to the list
            CustomAutocompleteItems.push({
                value: autocomplete_item_value,
                help_text: autocomplete_item_label,
                group: group_name,
                type: type,
            });
            if (type === "normal-variable") {
                // Add an unescaped version of the variable, too
                CustomAutocompleteItems.push({
                    value: autocomplete_item_value.replace(/^\{\{/u, "{{!"),
                    help_text: autocomplete_item_label,
                    group: group_name,
                    type: "unescaped-variable",
                });
            }
        });
    });
    if (error_messages.length > 0) {
        // Something failed
        return error_messages.join("; ");
    }
    // All ok
    return true;
}
/**
 * TODO: Can the sorting be removed from here? Now autocomplete items are sorted again every time when filtering, based on the keyword (https://github.com/Taitava/obsidian-shellcommands/issues/249).
 * @param autocomplete_item_sets
 */
function merge_and_sort_autocomplete_items(...autocomplete_item_sets) {
    const merged_autocomplete_items = (new Array()).concat(...autocomplete_item_sets);
    return merged_autocomplete_items.sort((a, b) => {
        // First compare groups
        if (a.group < b.group) {
            // a's group should come before b's group.
            return -1;
        }
        else if (a.group > b.group) {
            // a's group should come after b's group.
            return 1;
        }
        else {
            // The groups are the same.
            // Compare values.
            if (a.value < b.value) {
                // a should come before b.
                return -1;
            }
            else if (a.value > b.value) {
                // a should come after b.
                return 1;
            }
            else {
                // The values are the same.
                // The order does not matter.
                return 0;
            }
        }
    });
}
/**
 * Reduces an input string to the nearest logical word.
 * @param typed_text
 */
function get_search_query(typed_text) {
    const searchTextMatchArray = typed_text.match(/\S*?$/u); // An array, but only one match is expected.
    if (null === searchTextMatchArray) {
        throw new Error("get_search_query(): Regex match failed.");
    }
    let search_text = searchTextMatchArray[0]; // Reduce the text - limit to a single word (= exclude spaces and everything before them).
    let search_type = "other"; // May be overwritten.
    if (search_text.contains("}}")) {
        // The query happens right after a {{variable}}.
        // Make the query string to start after the }} pair, i.e. remove }} and everything before it. This improves the search.
        search_text = search_text.replace(/.+\}\}/u, "");
    }
    if (search_text.contains("{{")) {
        // A {{variable}} is being queried.
        // Make the query string to start from the {{ pair, i.e. remove everything before {{ . This improves the search.
        search_text = search_text.replace(/.+\{\{/u, "{{");
        if (search_text.contains("{{!")) {
            // An _unescaped_ variable is searched for.
            search_type = "unescaped-variable";
        }
        else {
            // A normal variable is searched for.
            search_type = "normal-variable";
        }
    }
    return {
        search_text: search_text,
        search_type: search_type,
    };
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function createPATHAugmentationFields(plugin, container_element, path_augmentations) {
    const path_variable_name = getPATHEnvironmentVariableName();
    new obsidian.Setting(container_element)
        .setName(`Add directories to the ${path_variable_name} environment variable`)
        .setHeading()
        .setDesc(`This is sometimes needed in order to be able to call some user installed applications. The directories will be appended AFTER the default directories in ${path_variable_name}, unless {{!environment:${path_variable_name}}} is included. Other {{variables}} can be used, too, but they don't affect the appending order.`)
        // An icon for showing the current PATH content.
        .addExtraButton(button => button
        .setIcon("bullet-list")
        .setTooltip(`Show the current ${path_variable_name} content (without any additions).`)
        .onClick(() => {
        if (undefined === process.env.PATH) {
            throw new Error("process.env.PATH is not a string.");
        }
        const modal = new ConfirmationModal(plugin, `Current ${path_variable_name} content`, process.env.PATH, "Close");
        modal.open();
    }))
        // Help link
        .addExtraButton(button => button
        .setIcon("help")
        .setTooltip(`Documentation: Additions to the ${path_variable_name} environment variable`)
        .onClick(() => gotoURL(Documentation.environments.additionsToPathEnvironmentVariable)));
    // Create a field for each operating system.
    const sub_container_element = container_element.createDiv();
    sub_container_element.addClass("SC-setting-group");
    Object.getOwnPropertyNames(PlatformNames).forEach((platform_id) => {
        const platform_name = PlatformNames[platform_id];
        new obsidian.Setting(sub_container_element).setName(platform_name + " " + getPATHEnvironmentVariableName(platform_id) + " additions")
            .setDesc("Define each directory on a separate line, or multiple directories on one line, separated by " + getVerbosePATHSeparator(platform_id))
            .addTextArea(textarea => textarea
            .setValue(path_augmentations[platform_id] ?? "")
            .onChange(async (new_path_augmentation) => {
            // PATH augmentation has been changed.
            // Update the configuration.
            if (new_path_augmentation.length > 0) {
                // The augmentation has content.
                path_augmentations[platform_id] = new_path_augmentation;
            }
            else {
                // The augmentation has been removed.
                delete path_augmentations[platform_id];
            }
            await plugin.saveSettings();
        })
            .then((textarea_component) => {
            // Add an autocomplete menu.
            createAutocomplete(plugin, textarea_component.inputEl, () => textarea_component.onChanged());
        }));
    });
    new obsidian.Setting(container_element)
        .setHeading()
        .setName("The " + path_variable_name + " additions do not work in Custom shells")
        .setDesc("They only work on shells that come predefined in this plugin. For instructions on how to do " + path_variable_name + " additions for custom shells, please click the documentation link on the right.")
        .addExtraButton(button => button
        .setIcon("help")
        .setTooltip("Documentation: " + path_variable_name + " additions for custom shells")
        .onClick(() => gotoURL(Documentation.environments.customShells.index + "#Path+additions")) // TODO: Create the section to the documentation page.
    );
}
/**
 * Determines a path separator character form the given platformId. Note that the decision is made by using whatever shell
 * happens to come up for that particular platformId. DO NOT use this function for real usages, it can only be used for
 * showing a preview to user of what will be used a separator. Do not export this function.
 *
 * Returns either:
 *  - "a colon :"
 *  - "a semicolon ;"
 *  - Or just a single character.
 *
 * @param platformId
 */
function getVerbosePATHSeparator(platformId) {
    const exampleShell = getShellsForPlatform(platformId).first(); // Does not matter so much which shell it is, just need to get one that can give a path separator for demonstration purposes.
    if (!exampleShell) {
        throw new Error("Was not able to get a shell for platform: " + platformId + ". A shell is needed for getting a path separator.");
    }
    const pathSeparator = exampleShell.getPathSeparator();
    switch (pathSeparator) {
        case ":":
            return "a colon :";
        case ";":
            return "a semicolon ;";
        default:
            // It's other than : or ; so don't know what word to add. Return as-is.
            return pathSeparator;
    }
}
function convertNewlinesToPATHSeparators(path, separator) {
    return path.replace(/(\r\n|\r|\n)+/gu, // + means that multiple adjacent newlines can be combined into a single separator character.
    () => separator);
}
/**
 * Retrieves a PATH environment variable augmentation string (specific to the current operating system) from the plugin's
 * configuration. Returns it WITHOUT parsing possible variables in the string. If the current operating system does not
 * have a dedicated PATH augmentation string in the configuration, returns null.
 *
 * @param plugin
 */
function getPATHAugmentation(plugin) {
    return plugin.settings.environment_variable_path_augmentations[getOperatingSystem()] ?? null;
}
/**
 * Returns OS specific name for the PATH environment variable. For Windows its Path, but for macOS and Linux its PATH, so
 * the only difference is casing.
 */
function getPATHEnvironmentVariableName(platform_id = getOperatingSystem()) {
    switch (platform_id) {
        case "darwin":
        case "linux":
            return "PATH";
        case "win32":
            return "Path";
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell {
    constructor(plugin) {
        this.plugin = plugin;
    }
    /**
     * Returns true if the given string is the same as the shell's binary file name.
     *
     * Child classes can extend this to detect also other, non filename based identifiers.
     *
     * @param shellIdentifier
     */
    matchesIdentifier(shellIdentifier) {
        return this.getIdentifier().toLocaleLowerCase() === shellIdentifier.toLocaleLowerCase();
    }
    getShellCommandWrapper() {
        if (this._getShellCommandWrapper) {
            return this._getShellCommandWrapper() ?? undefined; // If the call returns null, convert it to undefined. Null works well in CustomShellConfiguration, but undefined is used in ParsingProcess to denote that some content is not present for parsing.
        }
        return undefined;
    }
    /**
     * Quotes special characters in {{variable}} values according to an escaping mechanism defined for this shell.
     * Note that the value is returned intact if the shell does not support escaping. I'm looking at you, CMD.EXE! >:(
     *
     * @param rawValue
     */
    escapeValue(rawValue) {
        // Check if this Shell supports escaping
        const escaper = this.getEscaper(rawValue);
        if (escaper) {
            // Escaping is supported.
            return escaper.escape();
        }
        else {
            // No escaping is supported.
            // Return the value without modifications.
            return rawValue;
        }
    }
    /**
     * Executes the given shellCommandContent string using this shell. Retrieves spawning options from subclasses.
     *
     * @param shellCommandContent
     * @param workingDirectory
     * @param tShellCommand Used for getting default values when parsing shell arguments.
     * @param scEvent Allows using {{event_*}} variables in shell arguments.
     */
    async spawnChildProcess(shellCommandContent, workingDirectory, tShellCommand, scEvent) {
        // Allow subclasses to alter shellCommandContent and define other options.
        const spawnAugmentation = {
            shellCommandContent: shellCommandContent,
            spawnOptions: {},
            spawnArguments: [],
        };
        if (!await this.augmentSpawn(spawnAugmentation, tShellCommand, scEvent)) {
            // Something failed in the augmentation and execution should be cancelled. An error notification is already displayed.
            return null;
        }
        // Define working directory and environment variables.
        const extraSpawnOptions = {
            cwd: workingDirectory,
            env: this.getEnvironmentVariables(),
        };
        // Combine SpawnOptions. Do this after calling augmentSpawn(), so that it cannot accidentally override extraSpawnOptions.
        const combinedSpawnOptions = Object.assign(spawnAugmentation.spawnOptions, // Doesn't contain 'cwd' (= working directory) nor "env".
        extraSpawnOptions);
        // Execute the shell command.
        debugLog("Executing shell command: spawn(", spawnAugmentation.shellCommandContent, ",", combinedSpawnOptions, ",", spawnAugmentation.spawnArguments, ") ...");
        return child_process.spawn(spawnAugmentation.shellCommandContent, spawnAugmentation.spawnArguments, combinedSpawnOptions);
    }
    getEnvironmentVariables() {
        // Augment the PATH environment variable (if wanted and if this Shell has an augmentation method).
        const augmentedPath = this.augmentPATHEnvironmentVariable?.();
        if (augmentedPath && augmentedPath.length > 0) {
            // Define an object for environment variables.
            const environmentVariables = cloneObject(process.env); // Need to clone process.env, otherwise the modifications below will be stored permanently until Obsidian is hard-restarted (= closed and launched again).
            environmentVariables[getPATHEnvironmentVariableName()] = augmentedPath;
            return environmentVariables;
        }
        return undefined; // No need to augment environment variables.
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class BuiltinShell extends Shell {
    /**
     * Built-in shells use the path to the shell executable as their identifier in configuration files.
     */
    getIdentifier() {
        return this.getBinaryPath();
    }
    matchesIdentifier(shellIdentifier) {
        // First check if the base version of this method matches the identifier.
        if (super.matchesIdentifier(shellIdentifier)) {
            return true;
        }
        // Do an additional check on ownedShellBinaries.
        const shellFileName = extractFileName(shellIdentifier);
        return this.ownedShellBinaries.some((ownedShellBinary) => {
            return ownedShellBinary.toLocaleLowerCase() === shellFileName.toLocaleLowerCase();
        });
    }
    /**
     * Tells what shell binary to use during spawning.
     *
     * BuiltinShells do not need to alter the executable shell command, nor define any spawn arguments.
     *
     * @param spawnAugmentation
     * @protected
     */
    async augmentSpawn(spawnAugmentation) {
        spawnAugmentation.spawnOptions.shell = this.getBinaryPath();
        return true;
    }
    setEnvironmentVariablePathAugmentation(pathAugmentation) {
        this.pathAugmentation = pathAugmentation;
    }
    augmentPATHEnvironmentVariable() {
        const pathAugmentation = convertNewlinesToPATHSeparators(this.pathAugmentation ?? "", this.getPathSeparator());
        // Check if there's anything to augment.
        if (pathAugmentation.length > 0) {
            // Augment.
            const originalPath = process.env[getPATHEnvironmentVariableName()];
            if (undefined === originalPath) {
                throw new Error("process.env does not contain '" + getPATHEnvironmentVariableName() + "'.");
            }
            let augmentedPath;
            if (pathAugmentation.contains(originalPath)) {
                // The augmentation contains the original PATH.
                // Simply replace the whole original PATH with the augmented one, as there's no need to care about including
                // the original content.
                debugLog("Augmenting environment variable PATH so it will become " + pathAugmentation);
                augmentedPath = pathAugmentation;
            }
            else {
                // The augmentation does not contain the original PATH.
                // Instead of simply replacing the original PATH, append the augmentation after it.
                const separator = this.getPathSeparator();
                debugLog("Augmenting environment variable PATH by adding " + separator + pathAugmentation + " after it.");
                augmentedPath = originalPath + separator + pathAugmentation;
            }
            debugLog("PATH augmentation result: " + augmentedPath);
            return augmentedPath;
        }
        else {
            // No augmenting is needed.
            debugLog("No augmentation is defined for environment variable PATH. This is completely ok.");
            return "";
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell_Bash extends BuiltinShell {
    constructor() {
        super(...arguments);
        this.ownedShellBinaries = [
            "bash",
            "sh", // Sh might be something else than Bash, too, but make at least some Shell_* class recognise it. // TODO: Need to test that this works.
        ];
    }
    getEscaper(rawValue) {
        return new ShEscaper(rawValue);
    }
    getName() {
        return "Bash";
    }
    getBinaryPath() {
        return "/bin/bash";
    }
    getSupportedHostPlatforms() {
        return [
            "darwin",
            "linux",
        ];
    }
    getPathSeparator() {
        return ":";
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, false);
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, false);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell_Dash extends BuiltinShell {
    constructor() {
        super(...arguments);
        this.ownedShellBinaries = [
            "dash",
        ];
    }
    getEscaper(rawValue) {
        return new ShEscaper(rawValue);
    }
    getName() {
        return "Dash";
    }
    getBinaryPath() {
        return "/bin/dash";
    }
    getSupportedHostPlatforms() {
        return [
            "darwin",
            "linux",
        ];
    }
    getPathSeparator() {
        return ":";
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, false);
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, false);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell_Zsh extends BuiltinShell {
    constructor() {
        super(...arguments);
        this.ownedShellBinaries = [
            "zsh",
        ];
    }
    getEscaper(rawValue) {
        return new ShEscaper(rawValue);
    }
    getName() {
        return "Zsh (Z shell)";
    }
    getBinaryPath() {
        return "/bin/zsh";
    }
    getSupportedHostPlatforms() {
        return [
            "darwin",
            "linux",
        ];
    }
    getPathSeparator() {
        return ":";
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, false);
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, false);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class PowerShellEscaper extends AllSpecialCharactersEscaper {
    constructor() {
        super(...arguments);
        this.prefix = "`"; // In PowerShell, escaping should use a ` character, e.g. "Hello, world!" becomes `"Hello`,` world`!`"
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell_PowerShellCore extends BuiltinShell {
    constructor() {
        super(...arguments);
        this.ownedShellBinaries = [
            "pwsh.exe",
            "pwsh", // On Linux and Mac. (SC does not actually support using PowerShell on Linux/Mac just yet, but support can be added).
        ];
    }
    getEscaper(rawValue) {
        return new PowerShellEscaper(rawValue);
    }
    getName() {
        return "PowerShell Core";
    }
    getBinaryPath() {
        return "pwsh.exe";
    }
    /**
     * PowerShell Core could be used on all platforms, but SC currently supports it only on Windows.
     */
    getSupportedHostPlatforms() {
        return [
            "win32",
            // "darwin", // Support for these can be added later.
            // "linux",
        ];
    }
    getPathSeparator() {
        return ";";
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, isWindows()); // Use \ when on Windows, / on macOS and Linux.
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, isWindows()); // Use \ when on Windows, / on macOS and Linux.
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell_PowerShell5 extends BuiltinShell {
    constructor() {
        super(...arguments);
        this.ownedShellBinaries = [
            "PowerShell.exe",
        ];
    }
    getEscaper(rawValue) {
        return new PowerShellEscaper(rawValue);
    }
    getName() {
        return "PowerShell 5";
    }
    getBinaryPath() {
        return "PowerShell.exe";
    }
    getSupportedHostPlatforms() {
        return [
            "win32",
        ];
    }
    getPathSeparator() {
        return ";";
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, true);
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, true);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Shell_CMD extends BuiltinShell {
    constructor() {
        super(...arguments);
        this.ownedShellBinaries = [
            "CMD.EXE",
        ];
    }
    getEscaper(rawValue) {
        // :'(
        return null;
    }
    getName() {
        return "cmd.exe";
    }
    getBinaryPath() {
        return "CMD.EXE";
    }
    getSupportedHostPlatforms() {
        return [
            "win32",
        ];
    }
    getPathSeparator() {
        return ";";
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, true);
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, true);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * getShell() returns this "Shell" if it's passed an identifier string that matches no defined Shells. The unrecognized
 * shell identifier string comes from getUsersDefaultShellIdentifier(), which receives it from the current operating
 * system, so it's possible that the shell identifier is something that is not supported.
 *
 * This Shell tries to play along as long as nothing is executed (e.g. can be passed to parseVariables() to provide an
 * escaper - which is actually null, so it disables escaping). When a shell command is tried to be executed, this Shell
 * will show an error message telling that the shell identifier is not supported by the plugin, but the user might be able
 * to define the same shell as a CustomShell.
 *
 * UnsupportedShell must never be registered by calling registerShell(). It's only created in getShell() if needed.
 */
class UnsupportedShell extends Shell {
    constructor(plugin, unrecognisedShellIdentifier) {
        super(plugin);
        this.unrecognisedShellIdentifier = unrecognisedShellIdentifier;
    }
    matchesIdentifier(shellIdentifier) {
        return false; // UnsupportedShell is not picked up via normal matching, it's picked up by getShell() when no other shell matches.
    }
    getIdentifier() {
        return ""; // UnsupportedShell has no identifier of its own, as it can't be voluntarily selected as a Shell.
    }
    getBinaryPath() {
        return ""; // UnsupportedShell could return this.unrecognisedShellIdentifier as its binary path, but doesn't want to.
    }
    getEscaper(rawValue) {
        return null; // UnsupportedShell does not know what escaping mechanism it would prefer, so it chooses no escaping.
    }
    getName() {
        return "Unsupported shell: " + this.unrecognisedShellIdentifier;
    }
    getPathSeparator() {
        return isWindows() ? ";" : ":"; // UnsupportedShell can behave nicely when doing PATH environment variable augmentations.
    }
    getSupportedHostPlatforms() {
        return []; // UnsupportedShell is unsupported on all platforms.
    }
    translateAbsolutePath(originalPath) {
        return normalizePath2(originalPath, isWindows()); // UnsupportedShell does platform specific path normalizations for {{variable}} parsing in order not to break shell command preview texts.
    }
    translateRelativePath(originalPath) {
        return normalizePath2(originalPath, isWindows()); // UnsupportedShell does platform specific path normalizations for {{variable}} parsing in order not to break shell command preview texts.
    }
    async augmentSpawn(spawnAugmentation, tShellCommand, scEvent) {
        this.plugin.newError("This plugin does not support the following shell directly: " + this.unrecognisedShellIdentifier + ". Please consider setting it up as a custom shell in the plugin's settings. Then select the custom shell as a default for " + getCurrentPlatformName() + " in the plugin's settings.");
        return false;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
// Register shells
const shells = new Set;
function registerBuiltinShells(plugin) {
    registerShell(new Shell_Bash(plugin));
    registerShell(new Shell_Dash(plugin));
    registerShell(new Shell_Zsh(plugin));
    registerShell(new Shell_PowerShellCore(plugin));
    registerShell(new Shell_PowerShell5(plugin));
    registerShell(new Shell_CMD(plugin));
}
/**
 * I'm not sure if the name of this method should something else than getUsersDefaultShellIdentifier(). The 'identifier'
 * is a bit confusing, because the function returns a file path.
 */
function getUsersDefaultShellIdentifier() {
    if (isWindows()) {
        if (undefined === process.env.ComSpec) {
            throw new Error("process.env.ComSpec is not a string.");
        }
        return process.env.ComSpec;
    }
    else {
        if (undefined === process.env.SHELL) {
            throw new Error("process.env.SHELL is not a string.");
        }
        return process.env.SHELL;
    }
}
/**
 * @private Use this in getShell() only.
 */
const matchedShellsCache = new Map;
/**
 * Looks for a shell with the given filesystem path or id string. As the search is not as simple as just checking the equality of strings, the result is cached so that calling this multiple times should not cause overhead.
 *
 * @param plugin
 * @param shellIdentifier
 * @returns {{UnsupportedShell}} if a shell with the given identifier was not found. Otherwise, returns the queried Shell.
 */
function getShell(plugin, shellIdentifier) {
    const cachedShell = matchedShellsCache.get(shellIdentifier);
    if (cachedShell) {
        return cachedShell;
    }
    else {
        for (const shell of shells) {
            if (shell.matchesIdentifier(shellIdentifier)) {
                matchedShellsCache.set(shellIdentifier, shell);
                return shell;
            }
        }
        debugLog("getShell(): Didn't recognise " + shellIdentifier + ", so will return an UnsupportedShell.");
        // No Shell matched the given identifier. Return a no-can-do Shell that can be passed around in the program, but
        // that will show a visible error message if a shell command is tried to be executed with it.
        const unsupportedShell = new UnsupportedShell(plugin, shellIdentifier);
        matchedShellsCache.set(shellIdentifier, unsupportedShell);
        return unsupportedShell;
    }
}
function getShells() {
    return shells;
}
/**
 * Returns all Shells that support the given operating system.
 *
 * @param platformId
 */
function getShellsForPlatform(platformId) {
    return Array.from(shells).filter((shell) => {
        return shell.getSupportedHostPlatforms().includes(platformId);
    });
}
function registerShell(shell) {
    shells.add(shell);
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Variable_ShellCommandContent extends Variable {
    constructor(plugin, shellCommandContent) {
        super(plugin);
        this.shellCommandContent = shellCommandContent;
        this.variable_name = "shell_command_content";
        this.help_text = "Gives the executable shell command statements that should be passed to a shell.";
        this.always_available = true; // Make the variable not shown in default value settings. The default of this property is already true, but enforce this in case the default is some day changed to false.
    }
    /**
     * Used when parsing shell arguments.
     *
     * @protected
     */
    async generateValue() {
        return this.shellCommandContent;
    }
    /**
     * Used when doing shell command parsing, i.e. when wrapping parsed shell command content in a wrapper specified by a CustomShell.
     *
     * @protected
     */
    generateValueSynchronously() {
        return this.shellCommandContent;
    }
    getAvailabilityText() {
        return "<strong>Only available</strong> in  <em>custom shells'</em> settings: for defining shell arguments, or a shell command wrapper.";
    }
}

class Debouncer {
    constructor(plugin, configuration, tShellCommand) {
        this.plugin = plugin;
        this.configuration = configuration;
        this.tShellCommand = tShellCommand;
        this.state = "idle";
        this.subsequent = null;
        this.cooldownTimeout = null;
        if (!configuration) {
            throw new Error("Debouncer can only be instantiated with a shell command that has `debounce` enabled.");
        }
    }
    async executeWithDebouncing(scEvent) {
        switch (this.state) {
            case "idle":
                // IDLE PHASE.
                switch (this.getMode()) {
                    case "early-execution":
                    case "early-and-late-execution": {
                        // Execute immediately.
                        await this.execute(scEvent);
                        break;
                    }
                    case "late-execution": {
                        // Begin a cooldown phase and execute after it.
                        this.debugLog("is delayed.");
                        this.subsequent = {
                            scEvent: scEvent,
                        };
                        await this.cooldown();
                        break;
                    }
                }
                break;
            default:
                // EXECUTING OR COOLDOWN PHASE.
                if ("cooldown" === this.state && this.configuration.prolongCooldown) {
                    // Prolong cooldown duration.
                    this.prolongCooldownTimeout();
                }
                switch (this.getMode()) {
                    case "early-execution": {
                        // Nothing to do - just discard this execution.
                        this.debugLog("execution is discarded.");
                        break;
                    }
                    case "late-execution": {
                        // Wait until previous execution is over, then start another cooldown phase + execution.
                        this.debugLog("execution is postponed and may be merged to a later one.");
                        this.subsequent = {
                            scEvent: scEvent,
                        };
                        break;
                    }
                    case "early-and-late-execution": {
                        // Wait until previous execution is over and a cooldown phase is passed, too.
                        this.debugLog("execution is postponed and may be merged to a later one.");
                        this.subsequent = {
                            scEvent: scEvent,
                        };
                        break;
                    }
                }
                break;
        }
    }
    async execute(scEvent) {
        this.state = "executing";
        this.debugLog("will be executed now.");
        const executor = new ShellCommandExecutor(this.plugin, this.tShellCommand, scEvent);
        await executor.doPreactionsAndExecuteShellCommand();
        await this.afterExecuting();
    }
    async afterExecuting() {
        this.debugLog("execution ended.");
        switch (this.getMode()) {
            case "early-execution": {
                // Not much to do anymore, go to cooldown and clear state after it.
                await this.cooldown();
                break;
            }
            case "late-execution": {
                if (this.subsequent) {
                    // Another event triggering happened during execution. Start another cooldown + execution process.
                    await this.cooldown();
                }
                else {
                    // No events triggered during execution. Clear state.
                    this.state = "idle";
                }
                break;
            }
            case "early-and-late-execution": {
                // Go to cooldown and see after that if there's anything more to execute.
                await this.cooldown();
                break;
            }
        }
    }
    cooldown() {
        return new Promise((resolve) => {
            this.state = "cooldown";
            this.debugLog("is in cooldown phase now.");
            this.cooldownTimeout = this.createCooldownTimeout(() => { this.afterCooldown().then(resolve); }, true);
        });
    }
    async afterCooldown() {
        const debugMessageBase = "\"cooldown\" phase ended, ";
        this.cooldownTimeout = null;
        switch (this.getMode()) {
            case "early-execution": {
                // Not much to do after cooldown.
                this.debugLog(debugMessageBase + "debouncing ended.");
                this.state = "idle";
                break;
            }
            case "late-execution":
            case "early-and-late-execution": {
                if (this.subsequent) {
                    // There is a next execution waiting to be started.
                    this.debugLog(debugMessageBase + "will start a previously postponed execution.");
                    const executeWithEvent = this.subsequent.scEvent;
                    this.subsequent = null;
                    await this.execute(executeWithEvent);
                }
                else {
                    // No need to start another execution process. (We should only end up here in mode "early-and-late-execution", not in mode "late-execution").
                    this.debugLog(debugMessageBase + "no postponed execution is waiting, so will not re-execute.");
                    this.state = "idle";
                    this.subsequent = null;
                }
                break;
            }
        }
    }
    createCooldownTimeout(callback, returnObject) {
        const timeoutId = window.setTimeout(callback, this.getCoolDownMilliseconds());
        if (returnObject) {
            return {
                timeoutId: timeoutId,
                callback: callback,
            };
        }
        else {
            return timeoutId;
        }
    }
    prolongCooldownTimeout() {
        if (this.cooldownTimeout) {
            // Delete and recreate the timeout.
            this.debugLog("\"cooldown\" phase will be prolonged.");
            window.clearTimeout(this.cooldownTimeout.timeoutId);
            this.cooldownTimeout.timeoutId = this.createCooldownTimeout(this.cooldownTimeout.callback, false);
        }
        else {
            // Can't find a timeout that should be prolonged.
            this.debugLog("\"cooldown\" phase tried to be prolonged, but no timeout function exists. Might be a bug.");
        }
    }
    /**
     * Translates this.configuration.executeEarly and this.configuration.executeLate to an earlier mode format that this
     * class still uses.
     * @private
     */
    getMode() {
        if (this.configuration.executeEarly && this.configuration.executeLate) {
            return "early-and-late-execution";
        }
        else if (this.configuration.executeEarly) {
            return "early-execution";
        }
        else if (this.configuration.executeLate) {
            return "late-execution";
        }
        else {
            // Debouncing is disabled.
            throw new Error("Debouncer.getMode(): Debouncing is disabled, but it was tried to be used.");
        }
    }
    getCoolDownMilliseconds() {
        return this.configuration.cooldownDuration * 1000;
    }
    debugLog(message) {
        debugLog("Debouncing control: Shell command id " + this.tShellCommand.getId() + " " + message);
    }
    static getDefaultConfiguration(executeEarly, executeLate) {
        return {
            executeEarly: executeEarly,
            executeLate: executeLate,
            cooldownDuration: 0,
            prolongCooldown: false,
        };
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * TODO: Rename this class. Replace the T prefix with something else. The T stands for Type (kind of like TFile from Obsidian), but this is not a type, this is a class. Maybe ShellCommandInstance? It's not the best name, but I can't come up with a better one now.
 */
class TShellCommand extends Cacheable {
    constructor(plugin, configuration) {
        super();
        this.debouncer = null;
        this.plugin = plugin;
        this.configuration = configuration;
        // Introduce the ID to an ID generator so that it won't accidentally generate the same ID again when creating new shell commands.
        getIDGenerator().addReservedID(configuration.id);
    }
    getPlugin() {
        return this.plugin;
    }
    /**
     * Use this when you need to alter the configuration values. if you only need to read configuration values, use get*()
     * methods instead.
     */
    getConfiguration() {
        return this.configuration;
    }
    getId() {
        return this.configuration.id;
    }
    getShellIdentifier() {
        // Check if the shell command has defined a specific shell.
        const shellIdentifier = this.configuration.shells[getOperatingSystem()];
        if (undefined === shellIdentifier) {
            // The shell command does not define an explicit shell.
            // Use a default shell from the plugin's settings.
            return this.plugin.getDefaultShellIdentifier();
        }
        else {
            // The shell command has an explicit shell defined.
            return shellIdentifier;
        }
    }
    getShellIdentifierForPlatform(platformId) {
        // Check if the shell command has defined a specific shell.
        const shellIdentifier = this.configuration.shells[platformId];
        if (undefined === shellIdentifier) {
            // The shell command does not define an explicit shell.
            // Use a default shell from the plugin's settings.
            return this.plugin.getDefaultShellIdentifierForPlatform(platformId);
        }
        else {
            // The shell command has an explicit shell defined.
            return shellIdentifier;
        }
    }
    getShell() {
        return getShell(this.plugin, this.getShellIdentifier());
    }
    /**
     * Returns the Shell that would be used if the shell command is executed on the given platform.
     *
     * @param platformId
     */
    getShellForPlatform(platformId) {
        const shellIdentifier = this.getShellIdentifierForPlatform(platformId);
        if (undefined === shellIdentifier) {
            // This shell command relies on the operating system's default shell.
            return null;
        }
        else {
            return getShell(this.plugin, shellIdentifier);
        }
    }
    /**
     * Returns null if all platform specific shell command contents are filled.
     */
    getShellForDefaultCommand() {
        const platformCandidates = this.getPlatformCandidatesForDefaultCommand();
        if (platformCandidates.length === 0) {
            return null;
        }
        const currentPlatformId = getOperatingSystem();
        let usePlatformId;
        if (platformCandidates.includes(currentPlatformId)) {
            // Current platform is among the ones that will execute the default command.
            usePlatformId = currentPlatformId;
        }
        else {
            // Current platform won't execute the default command, so pick whatever platform there happens to be.
            usePlatformId = platformCandidates[0];
        }
        return this.getShellForPlatform(usePlatformId) ?? this.plugin.getDefaultShellForPlatform(usePlatformId);
    }
    /**
     * Determines which operating system(s) end up executing the 'default' shell command content.
     *
     * @private Can be made public if needed.
     */
    getPlatformCandidatesForDefaultCommand() {
        const candidatePlatformIds = new Set(PlatformNamesMap.keys());
        const shellCommandContents = this.getPlatformSpecificShellCommands();
        for (const platformId of PlatformNamesMap.keys()) {
            if (shellCommandContents[platformId] !== undefined) {
                // The default shell command content is not used on this platform.
                candidatePlatformIds.delete(platformId);
            }
        }
        return [...candidatePlatformIds];
    }
    /**
     * TODO: Rename to getShellIdentifiers().
     */
    getShells() {
        return this.configuration.shells;
    }
    getShellIdentifiersAsSet() {
        return new Set(Object.values(this.configuration.shells));
    }
    /**
     * Returns a shell command string specific for the current operating system, or a generic shell command if this shell
     * command does not have an explicit version for the current OS.
     *
     * Does not include any possible Shell provided augmentations to the shell command content.
     */
    getShellCommandContent() {
        // Check if the shell command has defined a specific command for this operating system.
        const platformSpecificShellCommand = this.configuration.platform_specific_commands[getOperatingSystem()];
        if (undefined === platformSpecificShellCommand) {
            // No command is defined specifically for this operating system.
            // Return an "OS agnostic" command.
            return this.configuration.platform_specific_commands.default;
        }
        else {
            // The shell command has defined a specific command for this operating system.
            return platformSpecificShellCommand;
        }
    }
    /**
     * Returns a version of the shell command that should be used if no platform specific command is defined for the
     * current platform. If you plan to use this for execution, consider using getShellCommand() instead, as it takes the
     * current platform into account.
     */
    getDefaultShellCommand() {
        return this.configuration.platform_specific_commands.default;
    }
    getPlatformSpecificShellCommands() {
        return this.configuration.platform_specific_commands;
    }
    /**
     * Returns a list of PlatformIds that have a shell command version defined. 'default' is never included in the list.
     *
     * TODO: Invent a better name for this method.
     */
    getNonEmptyPlatformIds() {
        const platform_specific_shell_commands = this.getPlatformSpecificShellCommands();
        const platform_ids_with_non_empty_shell_commands = [];
        let platform_id;
        for (platform_id in PlatformNames) { // Note that this loop does not iterate 'default' platform id (= the fallback platform id that is used when a shell command does not have a version for the current platform).
            const platform_specific_shell_command = platform_specific_shell_commands[platform_id];
            if (platform_specific_shell_command && "" !== platform_specific_shell_command.trim()) {
                platform_ids_with_non_empty_shell_commands.push(platform_id);
            }
        }
        return platform_ids_with_non_empty_shell_commands;
    }
    getIconId() {
        return this.configuration.icon;
    }
    getIconHTML() {
        if (this.configuration.icon) {
            // An icon is defined.
            return getIconHTML(this.configuration.icon);
        }
        else {
            // No icon is defined.
            return "";
        }
    }
    getAlias() {
        return this.configuration.alias;
    }
    /**
     * TODO: Use this method in all places where similar logic is needed. I guess generateObsidianCommandName() is the only place left.
     */
    getAliasOrShellCommand() {
        return this.configuration.alias || this.getShellCommandContent(); // TODO: Use this.getAlias().
    }
    static getAliasOrShellCommandContentFromParsingResult(parsingProcess) {
        const parsingResults = parsingProcess.getParsingResults();
        /** Don't confuse this name with ShellCommandParsingResult interface! The properties are very different. TODO: Rename ShellCommandParsingResult to something else. */
        const shellCommandParsingResult = parsingResults.shellCommandContent; // Use 'as' to denote that properties exist on this line and below.
        const aliasParsingResult = parsingResults.alias;
        const parsedShellCommand = shellCommandParsingResult.parsed_content;
        const parsedAlias = aliasParsingResult.parsed_content;
        return parsedAlias ? parsedAlias : parsedShellCommand;
    }
    getConfirmExecution() {
        return this.configuration.confirm_execution;
    }
    getIgnoreErrorCodes() {
        return this.configuration.ignore_error_codes;
    }
    getInputChannels() {
        return this.configuration.input_contents;
    }
    getOutputChannelOrder() {
        return this.configuration.output_channel_order;
    }
    getOutputHandlers() {
        return this.configuration.output_handlers;
    }
    getOutputHandlingMode() {
        return this.configuration.output_handling_mode;
    }
    /**
     * Finds an output wrapper that should be used for the given OutputStream. Returns null, if no OutputWrapper should
     * be used.
     *
     * @param output_stream
     */
    getOutputWrapper(output_stream) {
        const output_wrapper_id = this.configuration.output_wrappers[output_stream];
        if (!output_wrapper_id) {
            // No output wrapper is defined for this output stream in this shell command.
            return null;
        }
        for (const output_wrapper of this.plugin.getOutputWrappers().values()) {
            // Check if this is the output wrapper defined for this shell command.
            if (output_wrapper.getID() === output_wrapper_id) {
                // The correct output wrapper was found.
                return output_wrapper;
            }
        }
        throw new Error("OutputWrapper with ID " + output_wrapper_id + " was not found.");
    }
    /**
     * Checks if different output streams can be wrapped together. In addition to this, combining output streams also
     * requires the OutputChannels to be the same, but that's not checked in this method.
     */
    isOutputWrapperStdoutSameAsStderr() {
        return this.configuration.output_wrappers["stdout"] === this.configuration.output_wrappers["stderr"];
    }
    getExecutionNotificationMode() {
        if (null === this.configuration.execution_notification_mode) {
            // Get the mode from the main configuration.
            return this.plugin.settings.execution_notification_mode;
        }
        else {
            // This shell command defines its own mode.
            return this.configuration.execution_notification_mode;
        }
    }
    getEventsConfiguration() {
        return this.configuration.events;
    }
    getEventConfiguration(sc_event) {
        return this.getEventsConfiguration()[sc_event.static().getCode()] || sc_event.getDefaultConfiguration(false);
    }
    isSC_EventEnabled(event_code) {
        const events_configuration = this.getEventsConfiguration();
        if (undefined === events_configuration[event_code]) {
            // Not enabled
            return false;
        }
        else {
            // Maybe enabled
            return events_configuration[event_code].enabled;
        }
    }
    /**
     * Called when changing event settings in ShellCommandExtraOptionsModal.
     * plugin.saveSettings() needs to be called after this!
     *
     * @param sc_event
     */
    enableSC_Event(sc_event) {
        const event_code = sc_event.static().getCode();
        const events_configuration = this.getEventsConfiguration();
        if (undefined === events_configuration[event_code]) {
            // Not enabled
            // Enable
            events_configuration[event_code] = sc_event.getDefaultConfiguration(true);
        }
        else {
            // Maybe enabled
            if (!events_configuration[event_code].enabled) {
                events_configuration[event_code].enabled = true;
            }
        }
        if (sc_event.canRegisterAfterChangingSettings()) {
            this.registerSC_Event(sc_event);
        }
        sc_event.onAfterEnabling(this);
    }
    /**
     * Called when changing event settings in ShellCommandExtraOptionsModal.
     * plugin.saveSettings() needs to be called after this!
     *
     * @param sc_event
     */
    disableSC_Event(sc_event) {
        const event_code = sc_event.static().getCode();
        const events_configuration = this.getEventsConfiguration();
        if (undefined !== events_configuration[event_code]) {
            // Maybe enabled
            if (events_configuration[event_code].enabled) {
                // Is enabled.
                // Disable.
                const configuration_property_names = Object.getOwnPropertyNames(events_configuration[event_code]);
                if (configuration_property_names.length > 1) {
                    // There's more settings than just 'enable'.
                    // Disable by setting 'enable' to false, don't flush the settings, they can be useful if the event gets re-enabled.
                    events_configuration[event_code].enabled = false;
                }
                else {
                    // 'enabled' is the only setting.
                    // Disable by removing the configuration object completely to make the settings file cleaner.
                    delete events_configuration[event_code];
                }
            }
        }
        if (sc_event.canRegisterAfterChangingSettings()) {
            this.unregisterSC_Event(sc_event);
        }
    }
    /**
     * Returns all SC_Events that are enabled fro this shell command.
     *
     * Private as it's currently only used domestically, but can be changed to public if needed.
     */
    getSC_Events() {
        const enabled_sc_events = [];
        getSC_Events(this.plugin).forEach((sc_event) => {
            if (this.isSC_EventEnabled(sc_event.static().getCode())) {
                enabled_sc_events.push(sc_event);
            }
        });
        return enabled_sc_events;
    }
    /**
     * Private, if you need access from outside, use enableSC_Event().
     *
     * @param sc_event
     * @private
     */
    registerSC_Event(sc_event) {
        sc_event.register(this);
    }
    /**
     * Private, if you need access from outside, use disableSC_Event().
     *
     * @param sc_event
     * @private
     */
    unregisterSC_Event(sc_event) {
        sc_event.unregister(this);
    }
    /**
     * Set's up all events that are enabled for this shell command.
     *
     * @param called_after_changing_settings Set to: true, if this happens after changing configuration; false, if this happens during loading the plugin.
     */
    registerSC_Events(called_after_changing_settings) {
        this.getSC_Events().forEach((sc_event) => {
            const can_register = !called_after_changing_settings || sc_event.canRegisterAfterChangingSettings();
            if (can_register) {
                this.registerSC_Event(sc_event);
            }
        });
    }
    unregisterSC_Events() {
        this.getSC_Events().forEach((sc_event) => {
            this.unregisterSC_Event(sc_event);
        });
    }
    async executeWithDebouncing(scEvent) {
        if (!this.isDebouncingEnabled()) {
            throw new Error("Cannot call TShellCommand.executeWithDebouncing() if debouncing is not enabled.");
        }
        if (!this.debouncer) {
            this.debouncer = new Debouncer(this.plugin, this.configuration.debounce, this);
        }
        await this.debouncer.executeWithDebouncing(scEvent);
    }
    isDebouncingEnabled() {
        return (!!this.configuration.debounce) && (this.configuration.debounce.executeEarly || this.configuration.debounce.executeLate);
    }
    /**
     * If debouncer configuration is removed, this should be called (to remove Debouncer). Otherwise, the Debouncer would keep an old, stale configuration object in memory.
     * Next time debouncing is needed, a new Debouncer will be created automatically.
     */
    resetDebouncer() {
        this.debouncer = null;
    }
    /**
     * Called when executing a shell command from command palette. Could probably be called in other execution situations, too.
     *
     * @param parsing_process
     */
    async executeOrShowErrors(parsing_process) {
        if (!parsing_process) {
            parsing_process = this.createParsingProcess(null); // No SC_Event is available when executing shell commands via the command palette / hotkeys.
            // Try to process variables that can be processed before performing preactions.
            await parsing_process.process();
        }
        const parsingResults = parsing_process.getParsingResults();
        const shellCommandContentParsingSucceeded = parsingResults.shellCommandContent?.succeeded; // .shellCommandContent should always be present (even if parsing did not succeed), but if it's not, show errors in the else block.
        const shellCommandWrapperParsingSucceeded = parsingResults.shellCommandWrapper ? parsingResults.shellCommandWrapper.succeeded : true; // If no wrapper is present, pass.
        if (shellCommandContentParsingSucceeded && shellCommandWrapperParsingSucceeded) { // FIXME: This should not rely on just one (or two) content's parsing result, it should check all of them. Use parsing_process.getErrorMessages().length === 0 to check all parsed content.
            // The command was parsed correctly.
            const executor_instance = new ShellCommandExecutor(// Named 'executor_instance' because 'executor' is another constant.
            this.plugin, this, null // No SC_Event is available when executing via command palette or hotkey.
            );
            await executor_instance.doPreactionsAndExecuteShellCommand(parsing_process);
        }
        else {
            // The command could not be parsed correctly.
            // Display error messages
            parsing_process.displayErrorMessages();
        }
        // Memorize that this was the last shell command executed via command palette - even if parsing failed and
        // execution was cancelled. However, don't memorize, if this shell command should not appear in command palette.
        if (this.canShowInCommandPalette()) {
            this.plugin.lastTShellCommandExecutedFromCommandPalette = this;
        }
    }
    registerToCommandPalette() {
        const shell_command_id = this.getId();
        debugLog("Registering shell command #" + shell_command_id + "...");
        // Register an Obsidian command
        const obsidian_command = {
            id: this.plugin.generateObsidianCommandId(shell_command_id),
            name: generateObsidianCommandName(this.plugin, this.getAliasOrShellCommand()),
            // Use 'checkCallback' instead of normal 'callback' because we also want to get called when the command palette is opened.
            checkCallback: (is_opening_command_palette) => {
                if (is_opening_command_palette) {
                    // The user is currently opening the command palette.
                    // Check can the shell command be shown in command palette
                    if (!this.canShowInCommandPalette()) {
                        // Cancel preview and deny showing in command palette.
                        debugLog("Shell command #" + this.getId() + " won't be shown in command palette.");
                        return false;
                    }
                    // Do not execute the command yet, but parse variables for preview, if enabled in the settings.
                    debugLog("Getting command palette preview for shell command #" + this.getId());
                    if (this.plugin.settings.preview_variables_in_command_palette) {
                        // Preparse variables
                        const parsing_process = this.createParsingProcess(null); // No SC_Event is available when executing shell commands via the command palette / hotkeys.
                        parsing_process.process().then((parsing_succeeded) => {
                            if (parsing_succeeded) {
                                // Parsing succeeded
                                // Rename Obsidian command
                                this.renameObsidianCommand(TShellCommand.getAliasOrShellCommandContentFromParsingResult(parsing_process));
                                // Store the preparsed variables so that they will be used if this shell command gets executed.
                                this.plugin.cached_parsing_processes[this.getId()] = parsing_process;
                            }
                            else {
                                // Parsing failed, so use unparsed this.getAliasOrShellCommand().
                                this.renameObsidianCommand(this.getAliasOrShellCommand());
                                this.plugin.cached_parsing_processes[this.getId()] = undefined;
                            }
                        });
                    }
                    else {
                        // Parsing is disabled, so use unparsed this.getAliasOrShellCommand().
                        this.renameObsidianCommand(this.getAliasOrShellCommand());
                        this.plugin.cached_parsing_processes[this.getId()] = undefined;
                    }
                    return true; // Tell Obsidian this command can be shown in command palette.
                }
                else {
                    // The user has instructed to execute the command.
                    this.executeOrShowErrors(this.plugin.cached_parsing_processes[this.getId()]).then(() => {
                        // Delete the whole array of preparsed commands. Even though we only used just one command from it, we need to notice that opening a command
                        // palette might generate multiple preparsed commands in the array, but as the user selects and executes only one command, all these temporary
                        // commands are now obsolete. Delete them just in case the user toggles the variable preview feature off in the settings, or executes commands via hotkeys. We do not want to
                        // execute obsolete commands accidentally.
                        // This deletion also needs to be done even if the executed command was not a preparsed command, because
                        // even when preparsing is turned on in the settings, some commands may fail to parse, and therefore they would not be in this array, but other
                        // commands might be.
                        this.plugin.cached_parsing_processes = {}; // Removes obsolete preparsed variables from all shell commands.
                        return; // When we are not in the command palette check phase, there's no need to return a value. Just have this 'return' statement because all other return points have a 'return' too.
                    });
                }
            },
        };
        this.plugin.addCommand(obsidian_command);
        this.plugin.obsidian_commands[shell_command_id] = obsidian_command; // Store the reference so that we can edit the command later in ShellCommandsSettingsTab if needed. TODO: Use tShellCommand instead.
        this.setObsidianCommand(obsidian_command);
        debugLog("Registered.");
    }
    unregisterFromCommandPalette() {
        // FIXME: I think the unregistering does not work.
        delete this.plugin.obsidian_commands[this.getId()];
    }
    /**
     * Checks the configuration for command_palette_availability and returns:
     *  - true, if the value is "enabled" or "unlisted"
     *  - false, if the value is "disabled"
     *
     * Adding to command palette also enables hotkeys, which is why adding can be permitted, but showing denied, if a shell command should only be available via hotkeys.
     */
    canAddToCommandPalette() {
        return this.getConfiguration().command_palette_availability !== "disabled";
    }
    /**
     * Another name for canAddToCommandPalette().
     */
    canHaveHotkeys() {
        return this.canAddToCommandPalette();
    }
    /**
     * Checks the configuration for command_palette_availability and returns:
     *  - true, if the value is "enabled"
     *  - false, if the value is "disabled" or "unlisted"
     */
    canShowInCommandPalette() {
        return this.getConfiguration().command_palette_availability === "enabled";
    }
    /**
     * Creates a new ParsingProcess instance and defines two sets of variables:
     *  - First set: All variables that are not tied to any preactions.
     *  - Second set: Variables that are tied to preactions. Can be an empty set.
     * You need to still call ParsingProcess.process() to parse the first set. ShellCommandExecutor takes care of calling
     * ParsingProcess.processRest() to process all non-processed sets.
     *
     * @See ParsingProcess class for a description of the process.
     * @param sc_event Needed to get {{event_*}} variables parsed. Can be left out if working outside any SC_Event context, in which case {{event_*}} variables are inaccessible.
     */
    createParsingProcess(sc_event) {
        const stdout_output_wrapper = this.getOutputWrapper("stdout"); // Can be null
        const stderr_output_wrapper = this.getOutputWrapper("stderr"); // Can be null
        return new ParsingProcess(this.plugin, {
            shellCommandContent: this.getShellCommandContent(),
            shellCommandWrapper: this.getShell().getShellCommandWrapper(),
            alias: this.getAlias(),
            environment_variable_path_augmentation: getPATHAugmentation(this.plugin) ?? "",
            stdinContent: this.configuration.input_contents.stdin ?? undefined,
            output_wrapper_stdout: stdout_output_wrapper ? stdout_output_wrapper.getContent() : undefined,
            output_wrapper_stderr: stderr_output_wrapper ? stderr_output_wrapper.getContent() : undefined,
        }, this, sc_event, [
            this.getNonPreactionsDependentVariables(),
            this.getPreactionsDependentVariables(), // Second set: Variables that are tied to preactions. Can be an empty set.
        ], [
            // Do not escape variables in stdin, because shells won't interpret special characters in stdin. All characters are considered literal.
            "stdinContent",
            // Do not escape variables in output wrappers, because they are not going through a shell and escape characters would be visible in the end result.
            'output_wrapper_stdout',
            'output_wrapper_stderr',
        ]);
    }
    setObsidianCommand(obsidian_command) {
        this.obsidian_command = obsidian_command;
    }
    getObsidianCommand() {
        return this.obsidian_command;
    }
    /**
     * No renaming is done if the shell command is excluded from the command palette.
     */
    renameObsidianCommand(aliasOrShellCommandContent) {
        // Rename the command in command palette
        const prefix = this.plugin.getPluginName() + ": "; // Normally Obsidian prefixes all commands with the plugin name automatically, but now that we are actually _editing_ a command in the palette (not creating a new one), Obsidian won't do the prefixing for us.
        // Check that the shell command is actually registered to Obsidian's command palette.
        if (undefined !== this.obsidian_command) {
            // Yes, the shell command is registered in Obsidian's command palette.
            // Update the command palette name.
            this.obsidian_command.name = prefix + generateObsidianCommandName(this.plugin, aliasOrShellCommandContent);
        }
        // If the shell command's "command_palette_availability" settings is set to "disabled", then the shell command is not present in this.obsidian_command and so the command palette name does not need updating.
    }
    /**
     * Clears an internal cache used by .getPreactions().
     * Only needed to be called after creating new PreactionConfigurations or deleting old ones. Should not need to be called
     * when modifying properties in existing PreactionConfigurations.
     */
    resetPreactions() {
        debugLog(`TShellCommand ${this.getId()}: Resetting preactions.`);
        delete this.cached_preactions;
    }
    getPreactions() {
        debugLog(`TShellCommand ${this.getId()}: Getting preactions.`);
        if (!this.cached_preactions) {
            this.cached_preactions = [];
            let preaction_configuration;
            for (preaction_configuration of this.getConfiguration().preactions) {
                // Only create the preaction if it's enabled.
                if (preaction_configuration.enabled) {
                    // Yes, it's enabled.
                    // Instantiate the Preaction.
                    this.cached_preactions.push(createPreaction(this.plugin, preaction_configuration, this));
                }
            }
        }
        return this.cached_preactions;
    }
    /**
     * Returns Variables that are not dependent on any Preaction.
     * @private Can be made public if needed.
     */
    getNonPreactionsDependentVariables() {
        debugLog(`TShellCommand ${this.getId()}: Getting non preactions dependent variables.`);
        const all_variables = this.plugin.getVariables();
        return removeFromSet(all_variables, this.getPreactionsDependentVariables());
    }
    /**
     * @private Can be made public if needed.
     */
    getPreactionsDependentVariables() {
        debugLog(`TShellCommand ${this.getId()}: Getting preactions dependent variables.`);
        let dependent_variables = new VariableSet();
        for (const preaction of this.getPreactions()) {
            dependent_variables = mergeSets(dependent_variables, preaction.getDependentVariables());
        }
        return dependent_variables;
    }
    /**
     * @return Returns null, if no configuration is defined for this variable.
     * @param variable
     */
    getDefaultValueConfigurationForVariable(variable) {
        return this.configuration.variable_default_values[variable.getIdentifier()] ?? null;
    }
    /**
     * Returns an URI that can be used in links (in or outside of Obsidian) to execute this shell command. The URI also
     * contains stubs for any possible CustomVariables that might be used in the shell command (if any).
     */
    getExecutionURI() {
        const execution_uri = this.plugin.getObsidianURI(SC_Plugin.SHELL_COMMANDS_URI_ACTION, { execute: this.getId() });
        // Get a list CustomVariables that the shell command uses.
        const custom_variables = new VariableSet();
        const shellCommandWrapper = this.getShell().getShellCommandWrapper();
        const readVariablesFrom = shellCommandWrapper
            ? TShellCommand.wrapShellCommandContent(this.plugin, this.getShellCommandContent(), shellCommandWrapper, this.getShell())
            : this.getShellCommandContent() // Use unwrapped content.
        ;
        // FIXME: readVariablesFrom should actually include also other stuff that uses variables when executing shell commands, e.g. output wrappers. I think the best solution would be to call TShellCommand.createParsingProcess() and then get all variables from all the parseable content. Afterwards, delete the parsing process without actually parsing it, as the result would not be needed anyway. ParsingProcess class could have a new method named .getUsedVariables() that would wrap the global getUsedVariables() function and get variables used in that particular ParsingProcess.
        for (const custom_variable of getUsedVariables(this.plugin, readVariablesFrom).values()) { // Does not use this.getUsedCustomVariables(), because it doesn't include variables present in a possible wrapper, that a CustomShell might have defined.
            // Check that the variable IS a CustomVariable.
            if (custom_variable instanceof CustomVariable) { // TODO: Remove the check and pass only a list of CustomVariables to getUsedVariables().
                custom_variables.add(custom_variable);
            }
        }
        // Exclude variables whose values will come from Preactions - they will not probably be needed in the URI.
        const custom_variables_suitable_for_uri = removeFromSet(custom_variables, this.getPreactionsDependentVariables());
        // Append the suitable custom variable names to the uri.
        let execution_uri_with_variables = execution_uri;
        for (const custom_variable of custom_variables_suitable_for_uri) {
            execution_uri_with_variables += "&" + custom_variable.variable_name + "=";
        }
        // Finished.
        return execution_uri_with_variables;
    }
    /**
     * Returns a VariableMap containing Variables used in any of this TShellCommand's shell commands contents. Note that
     * variables used in Shell wrappers, preactions or output wrappers are not included.
     */
    getUsedCustomVariables() {
        return this.cache("getUsedCustomVariables", () => {
            // Gather parseable content.
            const readVariablesFrom = [
                ...Object.values(this.configuration.platform_specific_commands),
                this.getAlias() ?? "",
                this.configuration.input_contents.stdin ?? "",
                ...Object.values(this.configuration.variable_default_values).map((defaultValueConfiguration) => defaultValueConfiguration.value),
            ];
            return getUsedVariables(this.plugin, readVariablesFrom, this.plugin.getCustomVariables());
        });
    }
    /**
     * Returns an adjacent TShellCommand that appears next in the configuration list. Returns undefined, if this is the
     * last TShellCommand. Used in settings to switch quickly from one TShellCommand to another.
     */
    nextTShellCommand() {
        const t_shell_commands = Object.values(this.plugin.getTShellCommands());
        const this_index = t_shell_commands.indexOf(this);
        if (this_index === t_shell_commands.length - 1) {
            return undefined;
        }
        return t_shell_commands[this_index + 1];
    }
    /**
     * Returns an adjacent TShellCommand that appears before in the configuration list. Returns undefined, if this is the
     * first TShellCommand. Used in settings to switch quickly from one TShellCommand to another.
     */
    previousTShellCommand() {
        const t_shell_commands = Object.values(this.plugin.getTShellCommands());
        const this_index = t_shell_commands.indexOf(this);
        if (this_index === 0) {
            return undefined;
        }
        return t_shell_commands[this_index - 1];
    }
    /**
     * Replaces all occurrences of {{shell_command_content}} in shellCommandWrapper with shellCommandContent.
     *
     * @param plugin
     * @param shellCommandContent
     * @param shellCommandWrapper
     * @param shell
     */
    static wrapShellCommandContent(plugin, shellCommandContent, shellCommandWrapper, shell) {
        const debugMessageBase = `${this.constructor.name}.wrapShellCommandContent(): `;
        debugLog(`${debugMessageBase}Using wrapper: ${shellCommandWrapper} for shell command: ${shellCommandContent}`);
        // Wrap the shell command.
        const wrapperParsingResult = parseVariableSynchronously(shellCommandWrapper, new Variable_ShellCommandContent(plugin, shellCommandContent), shell);
        if (!wrapperParsingResult.succeeded) {
            // {{shell_command_content}} is so simple that there should be no way for its parsing to fail.
            throw new Error("{{shell_command_content}} parsing failed, although it should not fail.");
        }
        debugLog(`${debugMessageBase}Wrapped shell command: ${wrapperParsingResult.parsed_content}`);
        return wrapperParsingResult.parsed_content; // It's always string at this point, as .succeeded is checked above.
    }
}
class TShellCommandMap extends Map {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class ShellCommandExecutor {
    constructor(plugin, t_shell_command, 
    /** Needed for Preactions to be able to access all variables, in case any variables are used by a Preaction. Use null, if the shell command execution happens outside of any event context. */
    sc_event) {
        this.plugin = plugin;
        this.t_shell_command = t_shell_command;
        this.sc_event = sc_event;
    }
    /**
     * Performs preactions, and if they all give resolved Promises, executes the shell command.
     */
    async doPreactionsAndExecuteShellCommand(parsing_process, overriding_output_channel) {
        const preactions = this.t_shell_command.getPreactions();
        // Does an already started ParsingProcess exist?
        if (!parsing_process) {
            // No ParsingProcess yet.
            // Create one and parse all variables that are safe to parse before preactions.
            debugLog("Going to prepare possible Preactions, but will first start a variable parsing process. Depending on possible Preactions, this might not yet parse all variables.");
            parsing_process = this.t_shell_command.createParsingProcess(this.sc_event);
            // Parse the first set of variables, not all sets.
            if (!await parsing_process.process()) {
                // Some errors happened.
                debugLog("Will not prepare possible Preactions, because the parsing process failed. Will cancel shell command execution.");
                parsing_process.displayErrorMessages();
                return;
            }
        }
        else {
            debugLog("Going to prepare possible Preactions with an already started variable parsing process.");
        }
        // Create a pipeline for preactions.
        let preaction_pipeline = Promise.resolve(true); // Will contain a series of preaction performs.
        // Confirm execution from a user, if needed.
        // I haven't decided yet if I want to move this to be its own Preaction subclass. Might make sense, but requires configuration migration.
        if (this.t_shell_command.getConfiguration().confirm_execution) {
            preaction_pipeline = preaction_pipeline.then(() => {
                debugLog("Asking a confirmation from a user to execute shell command #" + this.t_shell_command.getId());
                return new Promise((resolve, reject) => {
                    const confirmation_modal = new ConfirmationModal(this.plugin, this.t_shell_command.getAliasOrShellCommand(), "Execute this shell command?", "Yes, execute");
                    confirmation_modal.open();
                    confirmation_modal.promise.then((execution_confirmed) => {
                        if (execution_confirmed) {
                            // The PromptModal has been closed.
                            // Check if user wanted to execute the shell command or cancel.
                            if (execution_confirmed) {
                                // User wants to execute.
                                debugLog("User confirmed to execute shell command #" + this.t_shell_command.getId());
                                resolve(true);
                            }
                            else {
                                // User wants to cancel.
                                debugLog("User cancelled execution of shell command #" + this.t_shell_command.getId());
                                resolve(false);
                            }
                        }
                    });
                });
            });
        }
        // Perform preactions
        preactions.forEach((preaction) => {
            debugLog(`Adding Preaction of type '${preaction.configuration.type}' to pipeline.`);
            preaction_pipeline = preaction_pipeline.then(() => {
                debugLog(`Calling Preaction of type '${preaction.configuration.type}'.`);
                if (!parsing_process) {
                    // Should have a ParsingProcess at this point.
                    throw new Error("No parsing process. Cannot do preaction.");
                }
                return preaction.perform(parsing_process, this.sc_event);
            });
        });
        if (0 === preactions.length) {
            debugLog("No Preactions to perform. This is ok.");
        }
        await preaction_pipeline.then(async (can_execute) => {
            if (can_execute) {
                // Parse either all variables, or if some variables are already parsed, then just the rest. Might also be that
                // all variables are already parsed.
                debugLog("Parsing all the rest of the variables (if there are any left).");
                if (!parsing_process) {
                    // Should have a ParsingProcess at this point.
                    throw new Error("No parsing process. Cannot execute shell command.");
                }
                if (await parsing_process.processRest()) {
                    // Parsing the rest of the variables succeeded
                    // Execute the shell command.
                    // TODO: Create a new class ShellCommandParsingProcess (extends ParsingProcess) whose .getParsingResults() returns the shell_command_parsing_result below. I.e. extract the parsing result conversion logic to a separate class. Note that the class should only accept shell_command_parsing_map (defined in TShellCommand.ts) as it's original_contents parameter/property.
                    const parsing_results = parsing_process.getParsingResults();
                    const unwrappedShellCommandContent = parsing_results.shellCommandContent.parsed_content;
                    const shell_command_parsing_result = {
                        unwrappedShellCommandContent: unwrappedShellCommandContent,
                        wrappedShellCommandContent: parsing_results.shellCommandWrapper?.parsed_content
                            ? TShellCommand.wrapShellCommandContent(this.plugin, unwrappedShellCommandContent, parsing_results.shellCommandWrapper.parsed_content, this.t_shell_command.getShell())
                            : unwrappedShellCommandContent // No wrapper, use unwrapped shell command content as wrapped.
                        ,
                        alias: parsing_results["alias"].parsed_content,
                        environment_variable_path_augmentation: parsing_results.environment_variable_path_augmentation.parsed_content,
                        stdinContent: parsing_results.stdinContent?.parsed_content,
                        output_wrapper_stdout: parsing_results.output_wrapper_stdout?.parsed_content,
                        output_wrapper_stderr: parsing_results.output_wrapper_stderr?.parsed_content,
                        succeeded: true,
                        error_messages: [],
                    };
                    debugLog("Will call ShellCommandExecutor.executeShellCommand().");
                    await this.executeShellCommand(shell_command_parsing_result, overriding_output_channel);
                }
                else {
                    // Parsing has failed.
                    debugLog("Parsing the rest of the variables failed.");
                    parsing_process.displayErrorMessages();
                }
            }
            else {
                // Cancel execution
                debugLog("Shell command execution cancelled.");
            }
        });
    }
    /**
     * Does not ask for confirmation before execution. This should only be called if: a) a confirmation is already asked from a user, or b) this command is defined not to need a confirmation.
     * Use confirmAndExecuteShellCommand() instead to have a confirmation asked before the execution.
     *
     * @param shell_command_parsing_result The actual shell command that will be executed is taken from this object's '.shell_command' property.
     * @param overriding_output_channel Optional. If specified, all output streams will be directed to this output channel. Otherwise, output channels are determined from this.t_shell_command.
     */
    async executeShellCommand(shell_command_parsing_result, overriding_output_channel) {
        const shell = this.t_shell_command.getShell();
        const working_directory = ShellCommandExecutor.getWorkingDirectory(this.plugin);
        // Define output channels
        let outputHandlers = this.t_shell_command.getOutputHandlers();
        if (overriding_output_channel) {
            // Ignore the shell command's normal output handlers and use temporarily something else.
            outputHandlers = {
                stdout: cloneObject(outputHandlers.stdout),
                stderr: cloneObject(outputHandlers.stderr), // ... accidentally editing the originals.
            };
            outputHandlers.stdout.handler = overriding_output_channel;
            outputHandlers.stderr.handler = overriding_output_channel;
        }
        // Check that the shell command is not empty
        if (!shell_command_parsing_result.unwrappedShellCommandContent.trim().length) { // Check unwrapped instead of wrapped so that can detect if the _actual_ shell command is empty. I.e. don't allow a Shell's wrapper to make an empty shell command non-empty.
            // It is empty
            const error_message = this.getErrorMessageForEmptyShellCommand();
            debugLog(error_message);
            this.plugin.newError(error_message);
            return;
        }
        // Check that the working directory exists and is a folder
        if (!fs__namespace.existsSync(working_directory)) {
            // Working directory does not exist
            // Prevent execution
            debugLog("Working directory does not exist: " + working_directory);
            this.plugin.newError("Working directory does not exist: " + working_directory);
        }
        else if (!fs__namespace.lstatSync(working_directory).isDirectory()) {
            // Working directory is not a directory.
            // Prevent execution
            debugLog("Working directory exists but is not a folder: " + working_directory);
            this.plugin.newError("Working directory exists but is not a folder: " + working_directory);
        }
        else {
            // Working directory is OK
            // Pass possible PATH augmentations to the Shell - if the Shell supports them.
            shell.setEnvironmentVariablePathAugmentation?.(shell_command_parsing_result.environment_variable_path_augmentation.length > 0
                ? shell_command_parsing_result.environment_variable_path_augmentation
                : undefined);
            // If setEnvironmentVariablePathAugmentation() does not exist, the Shell does not support PATH augmentation. Then just ignore the PATH augmentation.
            // - Only BuiltinShells support the PATH augmentation setting.
            // - CustomShells have so much better flexibility in their settings that there's no real need for them to support augmenting PATH via this setting.
            // - The PATH augmentation setting might be removed some day.
            // Execute the shell command
            const wrappedShellCommandContent = shell_command_parsing_result.wrappedShellCommandContent;
            try {
                const child_process = await shell.spawnChildProcess(wrappedShellCommandContent, working_directory, this.t_shell_command, this.sc_event);
                if (null === child_process) {
                    // No spawn() call was made due to some shell configuration error. Just cancel everything.
                    return;
                }
                // Pass stdin content (if defined)
                if (undefined !== shell_command_parsing_result.stdinContent) {
                    // Stdin content is defined
                    debugLog("Stdin content is present in parsing result. Will write it to the process.");
                    if (null === child_process.stdin) {
                        // noinspection ExceptionCaughtLocallyJS: The exception is caught locally below, but it's ok because it's then rethrown as the error message does not match '/spawn\s+ENAMETOOLONG/i'.
                        throw new Error("Shell command execution process does not have a standard input stream (stdin).");
                    }
                    child_process.stdin.write(shell_command_parsing_result.stdinContent);
                    child_process.stdin.end();
                }
                else {
                    debugLog("No stdin content is present in parsing result.");
                }
                // Common error handling regardless of output handling mode
                child_process.on("error", (error) => {
                    // Probably most errors will NOT end up here, I guess this event occurs for some rare errors.
                    //
                    // A quote from https://nodejs.org/api/child_process.html#event-error (read 2022-10-29):
                    // > The 'error' event is emitted whenever:
                    // > - The process could not be spawned, or
                    // > - The process could not be killed, or
                    // > - Sending a message to the child process failed.
                    debugLog("Shell command failed to execute: Received a non-stderr error message: " + error.message);
                    this.plugin.newError("Shell command failed to execute. Error: " + error.message);
                });
                // Define output encoding
                if (null === child_process.stdout || null == child_process.stderr) {
                    // The exception is caught locally below, but it's ok because it's then rethrown as the error message does not match '/spawn\s+ENAMETOOLONG/i'.
                    throw new Error("Child process's stdout and/or stderr stream is null.");
                }
                child_process.stdout.setEncoding("utf8"); // Receive stdout and ...
                child_process.stderr.setEncoding("utf8"); // ... stderr as strings, not as Buffer objects.
                // Define a terminator
                const processTerminator = () => {
                    child_process.kill("SIGTERM");
                };
                // Display a notification of the execution (if wanted).
                const executionNotificationMode = this.t_shell_command.getExecutionNotificationMode();
                if ("disabled" !== executionNotificationMode) {
                    this.showExecutionNotification(child_process, shell_command_parsing_result.unwrappedShellCommandContent, executionNotificationMode, processTerminator);
                }
                // Hook into child_process for output handling
                switch (this.t_shell_command.getOutputHandlingMode()) {
                    case "buffered": {
                        // Output will be buffered and handled as a single batch.
                        await this.handleBufferedOutput(child_process, shell_command_parsing_result, outputHandlers);
                        break;
                    }
                    case "realtime": {
                        // Output will be handled on-the-go.
                        await this.handleRealtimeOutput(child_process, shell_command_parsing_result, outputHandlers, processTerminator);
                    }
                }
            }
            catch (exception) {
                // An exception has happened.
                // Check if the shell command was too long.
                if (exception.message.match(/spawn\s+ENAMETOOLONG/i)) {
                    // It was too long. Show an error message.
                    this.plugin.newError("Shell command execution failed because it's too long: " + wrappedShellCommandContent.length + " characters. (Unfortunately the max limit is unknown).");
                }
                else {
                    // The shell command was not too long, this exception is about something else.
                    // Rethrow the exception.
                    throw exception;
                }
            }
        }
    }
    handleBufferedOutput(child_process, shell_command_parsing_result, outputChannels) {
        return new Promise((resolve) => {
            child_process.on("exit", (exitCode) => {
                // exitCode is null if user terminated the process. Reference: https://nodejs.org/api/child_process.html#event-exit (read on 2022-11-27).
                // Get outputs
                if (null === child_process.stdout || null == child_process.stderr) {
                    // The exception is caught locally below, but it's ok because it's then rethrown as the error message does not match '/spawn\s+ENAMETOOLONG/i'.
                    throw new Error("Child process's stdout and/or stderr stream is null.");
                }
                const stdout = child_process.stdout.read() ?? "";
                let stderr = child_process.stderr.read() ?? ""; // let instead of const: stderr can be emptied later due to ignoring.
                // Did the shell command execute successfully?
                if (exitCode === null || exitCode > 0) {
                    // Some error occurred
                    debugLog("Command executed and failed. Error number: " + exitCode + ". Stderr: " + stderr);
                    // Check if this error should be displayed to the user or not
                    if (null !== exitCode && this.t_shell_command.getIgnoreErrorCodes().contains(exitCode)) {
                        // The user has ignored this error.
                        debugLog("User has ignored this error, so won't display it.");
                        // Handle only stdout output stream
                        stderr = "";
                        exitCode = null; // TODO: consider if exitCode should just be left untouched. It could be informative to 'Ask after execution' output channel that shows exit code to user.
                    }
                    else {
                        // The error can be shown.
                        debugLog("Will display the error to user.");
                    }
                    // Handle at least stdout (and maybe stderr) output stream
                    handleBufferedOutput(this.plugin, this.t_shell_command, shell_command_parsing_result, stdout, stderr, exitCode, outputChannels);
                }
                else {
                    // Probably no errors, but do one more check.
                    // Even when 'error' is null and everything should be ok, there may still be error messages outputted in stderr.
                    if (stderr.length > 0) {
                        // Check a special case: should error code 0 be ignored?
                        if (this.t_shell_command.getIgnoreErrorCodes().contains(0)) {
                            // Exit code 0 is on the ignore list, so suppress stderr output.
                            stderr = "";
                            debugLog("Shell command executed: Encountered error code 0, but stderr is ignored.");
                        }
                        else {
                            debugLog("Shell command executed: Encountered error code 0, and stderr will be relayed to an output handler.");
                        }
                    }
                    else {
                        debugLog("Shell command executed: No errors.");
                    }
                    // Handle output
                    handleBufferedOutput(this.plugin, this.t_shell_command, shell_command_parsing_result, stdout, stderr, 0, outputChannels); // Use zero as an error code instead of null (0 means no error). If stderr happens to contain something, exit code 0 gets displayed in an error balloon (if that is selected as a channel for stderr).
                }
                // Output handling is finished.
                resolve();
            });
        });
    }
    handleRealtimeOutput(childProcess, shell_command_parsing_result, outputHandlerConfigurations, processTerminator) {
        return new Promise((resolve) => {
            // Prepare output channels
            const outputChannels = startRealtimeOutputHandling(this.plugin, this.t_shell_command, shell_command_parsing_result, outputHandlerConfigurations, processTerminator);
            // Define an output handler
            const handleNewOutputContent = async (outputStreamName, readableStream) => {
                if (null === childProcess.stdout || null == childProcess.stderr) {
                    throw new Error("Child process's stdout and/or stderr stream is null.");
                }
                // Don't emit new events while the current handling is in progress. (I think) it might cause a race condition where a simultaneous handling could overwrite another handling's data. Pause both streams, not just the current one, to maintain correct handling order also between the two streams.
                childProcess.stdout.pause();
                childProcess.stderr.pause();
                const outputContent = readableStream.read() ?? "";
                const outputChannel = outputChannels[outputStreamName];
                if (undefined === outputChannel) {
                    throw new Error("Output channel is undefined.");
                }
                await outputChannel.handleRealtime(outputStreamName, outputContent);
                // Can emit new events again.
                childProcess.stdout.resume();
                childProcess.stderr.resume();
            };
            // Hook into output streams' (such as stdout and stderr) output retrieving events.
            // Note that there might be just one stream, e.g. only stderr, if stdout is ignored. In the future, there might also be more than two streams, when custom streams are implemented.
            for (const outputStreamName of Object.getOwnPropertyNames(outputChannels)) {
                const readableStream = childProcess[outputStreamName];
                if (null === readableStream) {
                    throw new Error("Child process's readable stream '" + outputStreamName + "' is null.");
                }
                readableStream.on("readable", () => handleNewOutputContent(outputStreamName, readableStream));
            }
            // Hook into exit events
            childProcess.on("exit", (exitCode, signal /* TODO: Pass signal to channels so it can be shown to users in the future */) => {
                // Call all OutputChannels' endRealtime().
                const alreadyCalledChannelCodes = [];
                for (const outputStreamName of Object.getOwnPropertyNames(outputChannels)) {
                    const outputChannel = outputChannels[outputStreamName];
                    if (undefined === outputChannel) {
                        throw new Error("Output channel is undefined.");
                    }
                    const outputChannelCode = outputHandlerConfigurations[outputStreamName].handler;
                    // Ensure this OutputChannel has not yet been called.
                    if (!alreadyCalledChannelCodes.includes(outputChannelCode)) {
                        // Not yet called, so do the call.
                        outputChannel.endRealtime(exitCode);
                        // Mark that this channel's endRealtime() has already been called. Solves a situation where stderr and stdout uses the same channel, in which case endRealtime() should not be accidentally called twice.
                        alreadyCalledChannelCodes.push(outputChannelCode);
                    }
                }
                // Output handling is finished.
                resolve();
            });
        });
    }
    static getWorkingDirectory(plugin) {
        // Returns either a user defined working directory, or an automatically detected one.
        const working_directory = plugin.settings.working_directory;
        if (working_directory.length === 0) {
            // No working directory specified, so use the vault directory.
            return getVaultAbsolutePath(plugin.app);
        }
        else if (!path__namespace.isAbsolute(working_directory)) {
            // The working directory is relative.
            // Help to make it refer to the vault's directory. Without this, the relative path would refer to Obsidian's installation directory (at least on Windows).
            return path__namespace.join(getVaultAbsolutePath(plugin.app), working_directory);
        }
        return working_directory;
    }
    /**
     * This method should only be called if it's first checked that neither shell command version for the current platform nor a 'default' version exists.
     *
     * @private
     */
    getErrorMessageForEmptyShellCommand() {
        if (this.t_shell_command.getNonEmptyPlatformIds().length > 0) {
            // The shell command contains versions for other platforms, but not for the current one.
            const current_platform_name = getCurrentPlatformName();
            const version_word = this.t_shell_command.getNonEmptyPlatformIds().length > 1 ? "versions" : "a version";
            const other_platform_names = this.t_shell_command.getNonEmptyPlatformIds().map(platform_id => getPlatformName(platform_id)).join(" and ");
            return `The shell command does not have a version for ${current_platform_name}, it only has ${version_word} for ${other_platform_names}.`;
        }
        else {
            // The shell command doesn't contain a version for any platforms, it's completely empty.
            return "The shell command is empty. :(";
        }
    }
    /**
     * Displays a notification balloon indicating a user that a shell command is being executed.
     *
     * @param child_process
     * @param shell_command
     * @param execution_notification_mode
     * @param processTerminator Will be called if user clicks 'Request to terminate the process' icon.
     * @private
     */
    showExecutionNotification(child_process, shell_command, execution_notification_mode, processTerminator) {
        const createRequestTerminatingButton = (notice) => {
            // @ts-ignore Notice.noticeEl belongs to Obsidian's PRIVATE API, and it may change without a prior notice. Only
            // create the button if noticeEl exists and is an HTMLElement.
            const noticeEl = notice.noticeEl;
            if (undefined !== noticeEl && noticeEl instanceof HTMLElement) {
                this.plugin.createRequestTerminatingButton(noticeEl, processTerminator);
            }
        };
        const execution_notification_message = "Executing: " + (this.t_shell_command.getAlias() || shell_command);
        switch (execution_notification_mode) {
            case "quick": {
                // Retrieve the timeout from settings defined by a user.
                const processNotification = this.plugin.newNotification(execution_notification_message, undefined);
                createRequestTerminatingButton(processNotification);
                break;
            }
            case "permanent": {
                // Show the notification until the process ends.
                const processNotification = this.plugin.newNotification(execution_notification_message, 0);
                createRequestTerminatingButton(processNotification);
                // Hide the notification when the process finishes.
                child_process.on("exit", () => processNotification.hide());
                break;
            }
            case "if-long": {
                // Only show the notification if the process runs for an extended period of time (defined below).
                window.setTimeout(() => {
                    // Check if the process is still running.
                    if (null === child_process.exitCode && !child_process.killed) {
                        // The process is still running.
                        // Display notification.
                        const processNotification = this.plugin.newNotification(execution_notification_message, 0);
                        createRequestTerminatingButton(processNotification);
                        // Hide the notification when the process finishes.
                        child_process.on("exit", () => processNotification.hide());
                    }
                }, 2000); // If you change the timeout, change documentation, too!
                break;
            }
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Instance extends Cacheable {
    constructor(model, configuration, parent_instance_or_configuration) {
        super();
        this.model = model;
        this.configuration = configuration;
        debugLog(this.constructor.name + ": Creating a new instance.");
        // Determine parent type
        if (parent_instance_or_configuration instanceof Instance) {
            // It's an instance object
            this.parent_instance = parent_instance_or_configuration;
            this.parent_configuration = this.parent_instance.configuration;
        }
        else {
            // It's a configuration object.
            // No parent instance is available, so probably this is about SC_MainSettings object, as it does not have Model/Instance classes (at least yet).
            this.parent_instance = null; // It's null already, but do this just to make a statement.
            this.parent_configuration = parent_instance_or_configuration;
        }
    }
    /**
     * Returns a `VariableMap` containing all CustomVariables used by this Instance. The result is cached, and only
     * regenerated if configuration changes.
     */
    getUsedCustomVariables() {
        return this.cache("getUsedCustomVariables", () => this._getUsedCustomVariables());
    }
    setIfValid(field, value) {
        return this.model.validateValue(this, field, value).then(() => {
            this.configuration[field] = value;
        });
    }
    /**
     * Returns a UsageContainer containing a list of places where this Instance is used. The result is cached, and only
     * regenerated if configuration changes.
     */
    getUsages() {
        return this.cache("getUsages", () => {
            // Check that a usage getter is defined by the subclass.
            if (this._getUsages) {
                return this._getUsages();
            }
            else {
                // No usage getter is defined. E.g. PromptField does not need usage tracking.
                // Return an empty UsageContainer.
                return new UsageContainer(this.getTitle()); // subjectName will not be used in practise when the UsageContainer is empty.
            }
        });
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Model {
    constructor(plugin) {
        this.plugin = plugin;
    }
    static getSingularName() {
        throw new Error(this.name + " must define getSingularName().");
    }
    static getPluralName() {
        return this.getSingularName() + "s";
    }
    createSettingFields(instance, parent_element, with_deletion = true) {
        debugLog(this.constructor.name + ": Creating setting fields.");
        // Create a container
        const setting_fields_container = parent_element.createDiv(); // Create a nested container that can be easily deleted if the instance is deleted.
        const main_setting_field = this._createSettingFields(instance, setting_fields_container);
        if (with_deletion) {
            main_setting_field.addExtraButton(button => button
                .setIcon("trash")
                .setTooltip("Delete this " + this.static().getSingularName().toLocaleLowerCase())
                .onClick(() => {
                // The trash icon has been clicked
                // Open up a modal asking for confirmation if the instance can be deleted from this.parent_configuration.
                const confirmation_modal = new ConfirmationModal(this.plugin, "Delete " + this.static().getSingularName().toLocaleLowerCase() + ": " + instance.getTitle(), "Are you sure you want to delete this " + this.static().getSingularName().toLocaleLowerCase() + "?", "Yes, delete");
                // Show additional information, if the instance is used somewhere.
                const usages = instance.getUsages();
                if (usages.hasUsages()) {
                    confirmation_modal.extraContent.appendChild(usages.toHTMLElement("long"));
                }
                // Let subclasses add additional information to the deletion modal.
                this.augmentDeletionConfirmationModal?.(confirmation_modal, instance);
                confirmation_modal.open();
                confirmation_modal.promise.then(async (deletion_confirmed) => {
                    if (deletion_confirmed) {
                        // User has confirmed the deletion.
                        // Delete the configuration and remove the instance from custom collections.
                        this.deleteInstance(instance);
                        // Delete setting fields.
                        setting_fields_container.remove();
                        // Save settings
                        await this.plugin.saveSettings();
                    }
                });
            }));
        }
        return main_setting_field;
    }
    /**
     * Deletes the instance from configuration, and calls _deleteChild() which will delete the instance from custom collections.
     *
     * Can be made public if needed.
     */
    deleteInstance(instance) {
        debugLog(this.constructor.name + ": Deleting an instance.");
        this._deleteInstance(instance);
        const relation = this.defineParentConfigurationRelation(instance);
        switch (relation.type) {
            // case "one-to-one": // TODO: Uncomment when first model that needs this is implemented.
            // This is a relation where 'key' points directly to the instance's configuration.
            // delete this.parent_configuration[this.relation.key];
            // break;
            case "one-to-many-index": {
                // This is a relation where 'key' points to an indexed array of instance configurations. Use 'index' to pick the correct instance configuration.
                instance.parent_configuration[relation.key].splice(relation.index, 1); // Do not use delete, as it would place null in the list.
                break;
            }
            case "one-to-many-id": {
                // This is a relation where 'key' points to an indexed array of instance configurations. Use 'id' to determine the correct index.
                const index = this.idToIndex(instance.parent_configuration[relation.key], relation.id);
                if (null === index) {
                    // Something went wrong
                    throw new Error(`${this.constructor.name}.deleteInstance(): Could not find an index for id ${relation.id}.`);
                }
                instance.parent_configuration[relation.key].splice(index, 1); // Do not use delete, as it would place null in the list.
                break;
            }
        }
    }
    idToIndex(configurations, id) {
        let result_index = null;
        configurations.forEach((instance_configuration, index) => {
            if (instance_configuration.id === id) {
                // This is the correct configuration.
                result_index = index;
            }
        });
        return result_index;
    }
    /**
     * This should delete the instance from custom collections. It should be overridden by all Instance classes that have deletable children.
     */
    _deleteInstance(instance) {
        throw new Error(this.constructor.name + ".deleteInstance(): This class does not override _deleteInstance() method. Maybe the class is not supposed to have children?");
    }
    static() {
        return this.constructor;
    }
}
// Model class collection
const model_classes = new Map();
function introduceModelClass(model_class) {
    model_classes.set(model_class.constructor.name, model_class);
}
function getModel(model_class_name) {
    return model_classes.get(model_class_name);
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputWrapper extends Instance {
    constructor(model, plugin, configuration, parent_configuration) {
        super(model, configuration, parent_configuration);
        this.model = model;
        this.plugin = plugin;
        this.configuration = configuration;
        this.parent_configuration = parent_configuration;
        // Introduce the ID to an ID generator so that it won't accidentally generate the same ID again when creating new OutputWrappers.
        getIDGenerator().addReservedID(configuration.id);
    }
    getID() {
        return this.configuration.id;
    }
    getTitle() {
        return this.configuration.title;
    }
    getContent() {
        return this.configuration.content;
    }
    getConfiguration() {
        return this.configuration;
    }
    _getUsages() {
        const usages = new UsageContainer(this.getTitle());
        for (const tShellCommand of this.plugin.getTShellCommandsAsMap().values()) {
            let outputStream;
            for (outputStream of ["stdout", "stderr"]) {
                if (tShellCommand.getConfiguration().output_wrappers[outputStream] === this.getID()) {
                    usages.addUsage({
                        title: tShellCommand.getAliasOrShellCommand(),
                    }, "shellCommands");
                }
            }
        }
        return usages;
    }
    /**
     * Returns {{variables}} used in the OutputWrapper's content.
     *
     * @protected
     */
    _getUsedCustomVariables() {
        // Gather parseable content.
        const readVariablesFrom = [
            this.configuration.content,
        ];
        return getUsedVariables(this.plugin, readVariablesFrom, this.plugin.getCustomVariables());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputWrapperSettingsModal extends SC_Modal {
    constructor(plugin, output_wrapper, 
    /** Can be undefined if the output wrapper is created from a place where there is no name element. */
    output_wrapper_name_setting, 
    /** If defined, a button will be added and on_after_approval() / on_after_cancelling() will be called depending on whether the button was clicked or not. */
    ok_button_text, on_after_approval, on_after_cancelling) {
        super(plugin);
        this.output_wrapper = output_wrapper;
        this.output_wrapper_name_setting = output_wrapper_name_setting;
        this.ok_button_text = ok_button_text;
        this.on_after_approval = on_after_approval;
        this.on_after_cancelling = on_after_cancelling;
        this.approved = false;
    }
    onOpen() {
        super.onOpen();
        const container_element = this.modalEl.createDiv();
        container_element.addClass("SC-setting-group"); // Make setting fields wider in this container.
        // Title
        const title_setting = new obsidian.Setting(container_element)
            .setName("Output wrapper title")
            .setDesc("Only used in settings, will not appear in output.")
            .addText(text => text
            .setValue(this.output_wrapper.getTitle())
            .onChange(async (new_title) => {
            this.output_wrapper.getConfiguration().title = new_title;
            await this.plugin.saveSettings();
            // Update the title in a name setting. (Only if the modal was created from a place where an OutputWrapper name element exists).
            this.output_wrapper_name_setting?.setName(new_title);
        }));
        const title_input_element = title_setting.controlEl.find("input");
        // Content
        const output_variable = new Variable_Output(this.plugin, ""); // For getting an autocomplete item.
        new obsidian.Setting(container_element)
            .setName("Content")
            .setDesc("Use {{output}} as a placeholder for text that will be received from a shell command. Other variables are available, too.")
            .addTextArea(textarea_component => textarea_component
            .setValue(this.output_wrapper.configuration.content)
            .onChange(async (new_content) => {
            this.output_wrapper.configuration.content = new_content;
            await this.plugin.saveSettings();
        })
            .then((textarea_component) => {
            // Autocomplete for Content.
            if (this.plugin.settings.show_autocomplete_menu) {
                createAutocomplete(this.plugin, textarea_component.inputEl, () => textarea_component.onChanged(), output_variable.getAutocompleteItems());
            }
        }));
        // Focus on the title field.
        title_input_element.focus();
        // Ok button
        const okButtonText = this.ok_button_text;
        if (okButtonText) {
            new obsidian.Setting(container_element)
                .addButton(button => button
                .setButtonText(okButtonText)
                .onClick(() => this.approve()));
        }
    }
    approve() {
        if (this.on_after_approval) {
            this.approved = true;
            this.on_after_approval();
        }
        this.close();
    }
    onClose() {
        super.onClose();
        // Call a cancelling hook if one is defined (and if the closing happens due to cancelling, i.e. the ok button is NOT clicked).
        if (!this.approved && this.on_after_cancelling) {
            this.on_after_cancelling();
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class OutputWrapperModel extends Model {
    constructor() {
        super(...arguments);
        this.output_wrappers = new OutputWrapperMap();
    }
    _createSettingFields(output_wrapper, container_element) {
        debugLog("Creating setting fields for an OutputWrapper instance.");
        const output_wrapper_name_setting = new obsidian.Setting(container_element)
            // Configuration button
            .setName(output_wrapper.getTitle())
            .addExtraButton(button => button
            .setTooltip("Define output wrapper content")
            .setIcon("gear")
            .onClick(() => {
            this.openSettingsModal(output_wrapper, output_wrapper_name_setting);
        }));
        return output_wrapper_name_setting;
    }
    defineParentConfigurationRelation(output_wrapper) {
        return {
            type: "one-to-many-id",
            key: "output_wrappers",
            id: output_wrapper.getID(),
        };
    }
    static getSingularName() {
        return "Output wrapper";
    }
    loadInstances(parent_configuration) {
        debugLog("Loading OutputWrapper instances.");
        this.output_wrappers = new OutputWrapperMap();
        parent_configuration.output_wrappers.forEach((output_wrapper_configuration) => {
            const output_wrapper = new OutputWrapper(this, this.plugin, output_wrapper_configuration, parent_configuration);
            this.output_wrappers.set(output_wrapper_configuration.id, output_wrapper);
        });
        return this.output_wrappers;
    }
    newInstance(parent_configuration) {
        debugLog("Creating a new OutputWrapper instance.");
        // TODO: Move this logic to the base Model class.
        // Setup a default configuration and generate an ID
        const output_wrapper_configuration = this.getDefaultConfiguration();
        // Instantiate an OutputWrapper
        const output_wrapper = new OutputWrapper(this, this.plugin, output_wrapper_configuration, this.plugin.settings);
        this.output_wrappers.set(output_wrapper.getID(), output_wrapper);
        // Store the configuration into plugin's settings
        parent_configuration.output_wrappers.push(output_wrapper_configuration);
        // Return the OutputWrapper
        return output_wrapper;
    }
    validateValue(output_wrapper, field, value) {
        // No validation is needed, I guess. 'Title' and 'content' can both be empty, although an empty title does not make sense.
        return Promise.resolve(undefined);
    }
    openSettingsModal(output_wrapper, output_wrapper_name_setting) {
        debugLog("Opening settings modal for an OutputWrapper instance.");
        const modal = new OutputWrapperSettingsModal(this.plugin, output_wrapper, output_wrapper_name_setting);
        modal.open();
    }
    getDefaultConfiguration() {
        return {
            id: getIDGenerator().generateID(),
            title: "",
            content: "",
        };
    }
    _deleteInstance(deletable_output_wrapper) {
        debugLog("Deleting an OutputWrapper instance.");
        // Remove the OutputWrapper from all TShellCommands that use it.
        const shell_commands = this.plugin.getTShellCommands();
        for (const shell_command_id in shell_commands) {
            const t_shell_command = shell_commands[shell_command_id];
            const output_wrappers = t_shell_command.getConfiguration().output_wrappers;
            Object.each(output_wrappers, (output_wrapper_id, output_stream) => {
                if (output_wrapper_id === deletable_output_wrapper.getID()) {
                    // A shell command uses the output wrapper that is about to be deleted.
                    // Configure the shell command not to use any output wrapper.
                    output_wrappers[output_stream] = null;
                }
            });
        }
        // Remove the OutputWrapper from this class's internal list.
        this.output_wrappers.delete(deletable_output_wrapper.getID());
    }
}
class OutputWrapperMap extends Map {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomShellInstance extends Instance {
    constructor(model, configuration, parent_configuration) {
        super(model, configuration, parent_configuration);
        this.model = model;
        // Introduce the ID to an ID generator so that it won't accidentally generate the same ID again when creating new CustomShellInstances.
        getIDGenerator().addReservedID(configuration.id);
        // Create an operative shell.
        this.createCustomShell();
        debugLog(`Loaded CustomShellInstance ${this.getId()}.`);
    }
    getId() {
        return this.configuration.id;
    }
    getTitle() {
        return this.configuration.name;
    }
    createCustomShell() {
        debugLog(`CustomShellInstance ${this.getId()}: Creating an operational CustomShell.`);
        this.customShell = new CustomShell(this.model.plugin, this);
        registerShell(this.customShell);
        return this.customShell;
    }
    getCustomShell() {
        return this.customShell;
    }
    /**
     * Returns all TShellCommands that currently use this custom shell on the given platform.
     *
     * TODO: Consider moving this method to Shell.
     *
     * @private Can be made public, if needed.
     */
    getTShellCommandsByPlatform(platformId) {
        return new TShellCommandMap(Array.from(this.model.plugin.getTShellCommandsAsMap()).filter((entry) => {
            const tShellCommand = entry[1];
            return tShellCommand.getShells()[platformId] === this.getId();
        }));
    }
    /**
     * Returns all TShellCommands that currently use this CustomShell on any platform.
     *
     * TODO: Consider moving this method to Shell.
     *
     * @private Can be made public, if needed.
     */
    getTShellCommands() {
        return new TShellCommandMap(Array.from(this.model.plugin.getTShellCommandsAsMap()).filter((entry) => {
            const tShellCommand = entry[1];
            return tShellCommand.getShellIdentifiersAsSet().has(this.getId());
        }));
    }
    /**
     * Returns a list of operating system ids that are configured to use this shell as their default shell.
     *
     * TODO: Consider moving this method to Shell.
     *
     * @private Can be made public, if needed.
     */
    getPlatformIdsUsingThisShellAsDefault() {
        const platformIdsUsingThisShell = [];
        const defaultShells = this.model.plugin.settings.default_shells;
        for (const platformId of Object.getOwnPropertyNames(defaultShells)) {
            if (defaultShells[platformId] === this.getId()) {
                platformIdsUsingThisShell.push(platformId);
            }
        }
        return platformIdsUsingThisShell;
    }
    /**
     * Marks the given host operating system as enabled in CustomShellConfiguration.
     *
     * Note that the caller should save plugin settings, it's not done by this method.
     *
     * @param newPlatformId
     */
    changeHostPlatformIfCan(newPlatformId) {
        const usages = this.getUsages();
        if (usages.hasUsages()) {
            // Cannot change the host platform because there are usages.
            return usages.toSingleLineText();
        }
        else {
            // Can change the host platform.
            this.configuration.host_platform = newPlatformId;
            // Ensure the host platform configuration contains all properties a default configuration contains. However,
            // default values MUST NOT override any possibly existing values.
            switch (newPlatformId) {
                case "win32":
                    ensureObjectHasProperties(this.configuration.host_platform_configurations, { win32: CustomShellModel.getDefaultHostPlatformWindowsConfiguration() });
                    break;
            }
            return true;
        }
    }
    /**
     * Returns a human-readable list of shell commands using this Shell, and platforms where this is a default Shell.
     *
     */
    _getUsages() {
        const usedByTShellCommands = this.getTShellCommands();
        const usedByPlatformDefaults = this.getPlatformIdsUsingThisShellAsDefault();
        const usages = new UsageContainer(this.getTitle());
        if (usedByTShellCommands.size > 0) {
            usages.addUsages(Array.from(usedByTShellCommands.values()).map((tShellCommand) => ({
                title: tShellCommand.getAliasOrShellCommand(),
            })), "shellCommands");
        }
        if (usedByPlatformDefaults.length > 0) {
            usages.addUsages(usedByPlatformDefaults.map(platformId => ({
                title: "Default shell for " + PlatformNamesMap.get(platformId),
            })), "platforms");
        }
        return usages;
    }
    _getUsedCustomVariables() {
        // Gather parseable content.
        const readVariablesFrom = [
            ...this.configuration.shell_arguments,
            this.configuration.shell_command_wrapper ?? "",
            this.configuration.shell_command_test ?? "",
        ];
        return getUsedVariables(this.model.plugin, readVariablesFrom, this.model.plugin.getCustomVariables());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * Makes a textarea grow and shrink based on the content height, and applies CSS styles on it to make it look more like an <input> element (not so much padding).
 */
function decorateMultilineField(plugin, textareaComponent, extraOnChange, minimumLines = 1) {
    const textareaElement = textareaComponent.inputEl;
    textareaElement.addClass("SC-multiline");
    const updateTextareaHeight = () => {
        // Resize the shell command textarea to match the amount of lines in it.
        const content = textareaElement.value;
        const placeholder = textareaElement.placeholder;
        const newlines_pattern = /\r\n|\r|\n/;
        const count_lines_in_shell_command = content.split(newlines_pattern).length;
        const count_lines_in_shell_command_placeholder = placeholder.split(newlines_pattern).length;
        let count_lines_final = Math.max(count_lines_in_shell_command, count_lines_in_shell_command_placeholder, minimumLines);
        if (plugin.settings.max_visible_lines_in_shell_command_fields) {
            // Limit the height so that the field will not take up too much space.
            count_lines_final = Math.min(plugin.settings.max_visible_lines_in_shell_command_fields, count_lines_final);
        }
        textareaElement.rows = count_lines_final;
    };
    updateTextareaHeight(); // Set a correct initial height.
    textareaComponent.onChange(() => {
        updateTextareaHeight(); // Update the height every time the field's value changes.
        if (extraOnChange) {
            extraOnChange(textareaElement.value);
        }
    });
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * Creates a multiline text field for inputting a shell command, and an automatic preview text for it, that shows parsed {{variables}}.
 *
 * @param plugin
 * @param container_element
 * @param setting_icon_and_name
 * @param shell_command Textual shell command content.
 * @param shell
 * @param t_shell_command Will only be used to read default value configurations. Can be null if no TShellCommand is available, but then no default values can be accessed.
 * @param show_autocomplete_menu TODO: Remove this parameter and always read it from plugin settings.
 * @param extra_on_change
 * @param onAfterPreviewGenerated Will be called the first time preview has been generated. If the preview is updated, this won't be called again. If repeated calling is needed, a parameter could be added, telling whether it's a first-time call or a repeated call.
 * @param shell_command_placeholder
 * @param extraAutocompleteItems
 * @constructor
 */
function CreateShellCommandFieldCore(plugin, container_element, setting_icon_and_name, shell_command, shell, t_shell_command, show_autocomplete_menu, extra_on_change, onAfterPreviewGenerated, shell_command_placeholder = "Enter your command", extraAutocompleteItems) {
    async function generatePreview(previewSetting) {
        previewSetting.setDesc(await getShellCommandPreview(plugin, shell_command, shell, t_shell_command));
    }
    async function on_change(newShellCommandContent) {
        shell_command = newShellCommandContent; // Make generatePreview() use the new shell command content.
        // Update preview
        await generatePreview(setting_group.preview_setting);
        // Let the caller extend this onChange, to preform saving the settings:
        extra_on_change(shell_command);
    }
    const setting_group = {
        name_setting: new obsidian.Setting(container_element)
            .setClass("SC-name-setting")
            .then((name_setting) => {
            name_setting.nameEl.innerHTML = setting_icon_and_name;
        }),
        shell_command_setting: new obsidian.Setting(container_element)
            .addTextArea(textareaComponent => {
            textareaComponent
                .setPlaceholder(shell_command_placeholder)
                .setValue(shell_command);
            decorateMultilineField(plugin, textareaComponent, on_change);
        })
            .setClass("SC-shell-command-setting"),
        preview_setting: new obsidian.Setting(container_element)
            .setClass("SC-preview-setting")
            .then(async (setting) => {
            await generatePreview(setting);
            onAfterPreviewGenerated?.();
        }),
        /**
         * Called after a TShellCommand's shell has been changed.
         * @param newShell
         */
        refreshPreview: async (newShell) => {
            shell = newShell; // Change shell.
            await generatePreview(setting_group.preview_setting);
        },
    };
    // Autocomplete menu
    if (show_autocomplete_menu) {
        createAutocomplete(plugin, setting_group.shell_command_setting.settingEl.find("textarea"), on_change, extraAutocompleteItems);
    }
    return setting_group;
}
/**
 *
 * @param plugin
 * @param shell_command Textual shell command content.
 * @param shell Can be null if it's unknown, which shell will be used for execution. This can happen when creating shell command fields for other operating systems.
 * @param t_shell_command Will only be used to read default value configurations. Can be null if no TShellCommand is available, but then no default values can be accessed.
 * @public Exported because createShellCommandField uses this.
 */
async function getShellCommandPreview(plugin, shell_command, shell, t_shell_command) {
    const parsing_result = await parseVariables(plugin, shell_command, shell ?? plugin.getDefaultShell(), // If no shell is provided (= previewing a shell command for another operating system that has no explicitly selected shell), use the current operating system's default shell just to get some configuration for escaping and directory separators etc.
    true, t_shell_command, null);
    let previewContent;
    if (!parsing_result.succeeded) {
        // Variable parsing failed.
        if (parsing_result.error_messages.length > 0) {
            // Return all error messages, each in its own line. (Usually there's just one message).
            previewContent = parsing_result.error_messages.join(os.EOL); // Newlines are converted to <br>'s below.
        }
        else {
            // If there are no error messages, then errors are silently ignored by user's variable configuration.
            // The preview can then show the original, unparsed shell command.
            previewContent = shell_command;
        }
    }
    else {
        // Variable parsing succeeded
        previewContent = parsing_result.parsed_content;
    }
    // Convert the preview text to a DocumentFragment.
    const documentFragment = new DocumentFragment();
    if ("" !== previewContent) {
        const previewContentLines = previewContent.split(/\r\n|\r|\n/g); // Don't use ( ) with | because .split() would then include the newline characters in the resulting array.
        for (const previewContentLine of previewContentLines) {
            if (documentFragment.firstChild !== null) {
                // If earlier content exists, add a separating <br>.
                documentFragment.createEl("br");
            }
            documentFragment.appendText(previewContentLine);
        }
    }
    // Show shell name.
    if (documentFragment.firstChild !== null) {
        // If earlier content exists, add a separating <br>.
        documentFragment.createEl("br");
    }
    documentFragment.createEl("small", { text: shell ? shell.getName() : 'Unknown shell', attr: { class: "SC-preview-shell-name" } });
    // Done.
    return documentFragment;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomShellSettingsModal extends SC_Modal {
    constructor(plugin, customShellInstance, 
    /** Can be undefined if the instance is created from a place where there is no name element. */
    nameSetting, 
    /** If defined, a button will be added and onAfterApproval() / onAfterCancelling() will be called depending on whether the button was clicked or not. */
    okButtonText, onAfterApproval, onAfterCancelling) {
        super(plugin);
        this.customShellInstance = customShellInstance;
        this.nameSetting = nameSetting;
        this.okButtonText = okButtonText;
        this.onAfterApproval = onAfterApproval;
        this.onAfterCancelling = onAfterCancelling;
        this.approved = false;
    }
    onOpen() {
        super.onOpen();
        const containerElement = this.modalEl;
        const titleAndDescriptionGroupElement = containerElement.createDiv({ attr: { class: "SC-setting-group" } });
        // Name
        const title_setting = new obsidian.Setting(titleAndDescriptionGroupElement)
            .setName("Shell name")
            .setDesc("A label used to select the shell in settings.")
            .addText(text => text
            .setValue(this.getCustomShellConfiguration().name)
            .onChange(async (newName) => {
            this.getCustomShellConfiguration().name = newName;
            await this.plugin.saveSettings();
            // Update the title in a name setting. (Only if the modal was created from a place where a CustomShellInstance name element exists).
            this.nameSetting?.setName(newName);
        }));
        const nameInputElement = title_setting.controlEl.find("input");
        // Focus on the name field.
        nameInputElement.focus();
        // Description
        new obsidian.Setting(titleAndDescriptionGroupElement)
            .setName("Description")
            .addTextArea(textarea => textarea
            .setValue(this.getCustomShellConfiguration().description)
            .onChange(async (newDescription) => {
            this.getCustomShellConfiguration().description = newDescription;
            await this.plugin.saveSettings();
            // Update the description in a name setting. (Only if the modal was created from a place where a CustomShellInstance name element exists).
            if (this.nameSetting) {
                this.nameSetting.setDesc(""); // Clear a possible old description.
                createMultilineTextElement("span", newDescription, this.nameSetting.descEl);
            }
        }));
        // Binary path.
        this.createBinaryPathSetting(containerElement.createDiv({ attr: { class: "SC-setting-group" } }));
        // Shell arguments.
        this.createShellArgumentsSetting(containerElement.createDiv({ attr: { class: "SC-setting-group" } }));
        // Supported operating systems
        const hostPlatformContainer = containerElement.createDiv({ attr: { class: "SC-setting-group" } });
        this.createHostPlatformField(hostPlatformContainer);
        // Shell operating system
        new obsidian.Setting(containerElement.createDiv({ attr: { class: "SC-setting-group" } }))
            .setName("Shell's operating system")
            .setDesc("If the shell virtualizes, uses as a subsystem, or otherwise emulates another operating system than the selected host operating system, select it here. This is used to make directory paths etc. work correctly.")
            .addDropdown(dropdownComponent => dropdownComponent
            .addOption("none", "Same as 'Host platform' (" + getPlatformName(this.getCustomShellConfiguration().host_platform) + ")")
            .addOptions(PlatformNames) // FIXME: Find a better way to tell TypeScript that PlatformNames is of a correct type.
            .setValue(this.getCustomShellConfiguration().shell_platform ?? "none")
            .onChange(async (newShellPlatform) => {
            switch (newShellPlatform) {
                case "none":
                    this.getCustomShellConfiguration().shell_platform = null;
                    break;
                default:
                    this.getCustomShellConfiguration().shell_platform = newShellPlatform;
            }
            await this.plugin.saveSettings();
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Shell's operating system")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Shell's+operating+system")));
        // Special characters escaping
        new obsidian.Setting(containerElement.createDiv({ attr: { class: "SC-setting-group" } }))
            .setName("Special characters escaping")
            .setDesc("Used to quote special characters (= other than alphabets, numbers and _) in {{variable}} values.")
            .addDropdown(dropdownComponent => dropdownComponent
            .addOptions({
            "UnixShell": "Unix shell style with \\ as escape character",
            "PowerShell": "PowerShell style with ` as escape character",
            "none": "No escaping (not recommended)",
        })
            .setValue(this.getCustomShellConfiguration().escaper ?? "none")
            .onChange(async (newEscaper) => {
            this.getCustomShellConfiguration().escaper = newEscaper === "none" ? null : newEscaper;
            await this.plugin.saveSettings();
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Special characters escaping")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Special+characters+escaping")));
        // Path translator.
        const pathTranslatorContainer = containerElement.createDiv({ attr: { class: "SC-setting-group" } });
        this.createPathTranslatorField(pathTranslatorContainer);
        // Shell command wrapper.
        this.createShellCommandWrapperField(containerElement);
        // Shell testing field.
        this.createShellTestField(containerElement);
    }
    createBinaryPathSetting(containerElement) {
        new obsidian.Setting(containerElement)
            .setName("Executable binary file path")
            .setDesc("This should only contain a directory and a file name (or just a file name), not any possible command line options/arguments. They will be configured below.")
            .addText(textComponent => textComponent
            .setValue(this.getCustomShellConfiguration().binary_path)
            .onChange(async (newBinaryPath) => {
            this.getCustomShellConfiguration().binary_path = newBinaryPath;
            await this.plugin.saveSettings();
            updateBinaryPathWarning();
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Executable binary file path")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Executable+binary+file+path")));
        const binaryPathWarningDescription = new obsidian.Setting(containerElement)
            .setClass("SC-full-description");
        const updateBinaryPathWarning = () => {
            const binaryPath = this.getCustomShellConfiguration().binary_path;
            if ("" === binaryPath) {
                // No binary path.
                binaryPathWarningDescription.setDesc("Note: No binary path is defined.");
                binaryPathWarningDescription.descEl.addClass("SC-text-right"); // Short texts should be aligned to the right, so that they are under the binary path input field.
            }
            else {
                // A binary path is defined.
                const actualBinaryPathExists = fs__namespace.existsSync(binaryPath);
                const appendedBinaryPathExists = !actualBinaryPathExists && lookUpFileWithBinaryExtensionsOnWindows(binaryPath); // No need to do lookup if actualBinaryPathExists.
                if (actualBinaryPathExists || appendedBinaryPathExists) {
                    // The binary path exists.
                    if (actualBinaryPathExists && fs__namespace.lstatSync(binaryPath).isDirectory()) { // Don't check for a folder if the file was determined by adding an extension to the file name, because fs.lstatSync() breaks if a non-existing path is passed to it.
                        // The binary path is a folder.
                        binaryPathWarningDescription.setDesc("Note: " + binaryPath + " is a directory. A file is expected.");
                    }
                    else {
                        // OK: A file exists.
                        binaryPathWarningDescription.setDesc("Good, " + binaryPath + " exists.");
                    }
                    binaryPathWarningDescription.descEl.addClass("SC-text-right"); // Short texts should be aligned to the right, so that they are under the binary path input field.
                }
                else {
                    // The binary path does not exist.
                    if (this.getCustomShellConfiguration().host_platform === getOperatingSystem()) {
                        // The shell is supposed to work on the current operating system.
                        const notExistsText = `Note: ${binaryPath} does not seem to exist.`;
                        const testShellText = " You can test the shell at the bottom of this modal.";
                        if (path__namespace.isAbsolute(binaryPath)) {
                            // The path is absolute, so it's probable that the shell won't work, as absolute binary paths are not likely to be found via the PATH environment variable.
                            binaryPathWarningDescription.setDesc(`${notExistsText} It's good that the path is absolute, but maybe it contains a typing error?${testShellText}`);
                        }
                        else {
                            // The path is relative, so it might be executable, if it happens to be findable via the PATH environment variable.
                            binaryPathWarningDescription.setDesc(`${notExistsText} However, it might still work if the operating system recognises it as an executable command.${testShellText} If you encounter problems, try using an absolute path, i.e. a full path starting from the root of the file system.`);
                        }
                    }
                    else {
                        // The shell is meant for another operating system, so it's understandable that the binary might not be present on this OS.
                        binaryPathWarningDescription.setDesc("Note: The shell is configured to be used on a different operating system than " + getCurrentPlatformName() + ", so the existence of the binary file cannot be reliably verified.");
                    }
                    binaryPathWarningDescription.descEl.removeClass("SC-text-right"); // Long texts should be aligned to the left.
                }
            }
        };
        updateBinaryPathWarning();
    }
    createShellArgumentsSetting(containerElement) {
        const shellCommandContentVariable = new Variable_ShellCommandContent(this.plugin, ""); // For getting an autocomplete item.
        new obsidian.Setting(containerElement)
            .setName("Shell arguments")
            .setDesc("Command line options/arguments to execute the shell's binary file with. The executable shell command should be one of them; " + shellCommandContentVariable.getFullName(true) + " provides it. Other {{variables}} are supported, too. Separate different arguments with a newline. Possible newlines coming from {{variable}} values are not considered as separators.")
            .addTextArea((textareaComponent) => textareaComponent
            .setValue(this.getCustomShellConfiguration().shell_arguments.join("\n"))
            .onChange(async (concatenatedShellArguments) => {
            this.getCustomShellConfiguration().shell_arguments = concatenatedShellArguments.split("\n");
            await this.plugin.saveSettings();
            updateNoShellCommandContentVariableWarning();
        })
            .then((textareaComponent) => {
            if (this.plugin.settings.show_autocomplete_menu) {
                createAutocomplete(this.plugin, textareaComponent.inputEl, () => textareaComponent.onChanged(), shellCommandContentVariable.getAutocompleteItems());
            }
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Shell arguments")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Shell+arguments")));
        const shellArgumentsWarningDescription = new obsidian.Setting(containerElement)
            .setClass("SC-full-description");
        const updateNoShellCommandContentVariableWarning = () => {
            const shellArguments = this.getCustomShellConfiguration().shell_arguments.join("\n"); // Then join glue does not really matter here.
            if (0 === getUsedVariables(this.plugin, shellArguments, shellCommandContentVariable).size) {
                // The arguments do not contain {{shell_command_content}}. Show a warning.
                shellArgumentsWarningDescription.setDesc(this.getShellCommandContentWarningText("arguments", shellCommandContentVariable));
            }
            else {
                // Clear a possible earlier warning.
                shellArgumentsWarningDescription.setDesc("");
            }
        };
        updateNoShellCommandContentVariableWarning();
    }
    createHostPlatformField(containerElement) {
        new obsidian.Setting(containerElement)
            .setName("Host operating system")
            .setDesc("The shell is only available when Obsidian runs on the selected operating system. Note that in case your shell utilizes a sub-operating system (e.g. Windows Subsystem for Linux, WSL), you still need to select the operating system Obsidian is running on, not the sub-system's operating system.")
            .addDropdown(dropdownComponent => dropdownComponent
            .addOptions(Object.fromEntries(PlatformNamesMap))
            .setValue(this.getCustomShellConfiguration().host_platform)
            .onChange(async (newHostPlatform) => {
            const changeResult = this.customShellInstance.changeHostPlatformIfCan(newHostPlatform);
            if ("string" === typeof changeResult) {
                // Cannot change the host platform, because the shell has usages.
                this.plugin.newError("Cannot change the host platform, because the shell is used " + changeResult + ".");
                dropdownComponent.setValue(this.getCustomShellConfiguration().host_platform); // Undo changing dropdown selection.
            }
            else {
                // The host platform was changed ok.
                await this.plugin.saveSettings();
            }
            // Show or hide any platform specific settings.
            updatePlatformSpecificSettingsVisibility(newHostPlatform);
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Host operating system")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Host+operating+system")));
        // Platform specific settings.
        const windowsSpecificSettings = this.createHostPlatformWindowsSpecificSettings(containerElement);
        const updatePlatformSpecificSettingsVisibility = (newHostPlatform) => {
            // Update Windows settings visibility.
            windowsSpecificSettings.forEach((setting) => setting.settingEl.toggleClass("SC-hide", "win32" !== newHostPlatform));
        };
        // Hide the settings immediately, if needed.
        updatePlatformSpecificSettingsVisibility(this.getCustomShellConfiguration().host_platform);
    }
    createHostPlatformWindowsSpecificSettings(containerElement) {
        /* Enable this heading if Windows will have more than one setting. Then remove the texts "Windows: " and "The setting doesn't affect macOS/Linux" below.
        new Setting(containerElement)
            .setName("Windows specific")
            .setHeading()
            .setDesc("These settings are only used when your workstation uses Windows.")
        ;
        */
        // 'Quote shell arguments' setting.
        const quoteShellArgumentsInitialValue = this.getCustomShellConfiguration().host_platform_configurations.win32?.quote_shell_arguments // Use value from user configuration if defined.
            ?? CustomShellModel.getDefaultHostPlatformWindowsConfiguration().quote_shell_arguments // Otherwise get a default value.
        ;
        const quoteShellArgumentsSetting = new obsidian.Setting(containerElement)
            .setName("Windows: Quote shell arguments")
            .setDesc('Wraps shell arguments in double quotes if they contain spaces (e.g. echo Hi becomes "echo Hi"). If arguments contain double quotes already, they are escaped by preceding them with a backslash (e.g. echo "Hi there" becomes "echo \\"Hi there\\""). If your shell complains that a command does not exist, try changing this setting. (The setting doesn\'t affect macOS/Linux. The quoting is done by Node.js, not by the SC plugin.)')
            .setClass("SC-wide-description") // .setClass() actually _adds_ a class, so this call...
            .setClass("SC-indent") // ...is not overridden by this call.
            .addToggle(toggleComponent => toggleComponent
            .setValue(quoteShellArgumentsInitialValue)
            .onChange(async (quoteShellArgumentsNewValue) => {
            const customShellConfiguration = this.getCustomShellConfiguration();
            if (undefined === customShellConfiguration.host_platform_configurations.win32) {
                // If win32 is not defined, create an object for it with default values - which can be overridden immediately.
                customShellConfiguration.host_platform_configurations.win32 = CustomShellModel.getDefaultHostPlatformWindowsConfiguration();
            }
            customShellConfiguration.host_platform_configurations.win32.quote_shell_arguments = quoteShellArgumentsNewValue;
            await this.plugin.saveSettings();
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Windows: Quote shell arguments")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Windows%3A+Quote+shell+arguments")));
        // Return all created settings for visibility control.
        return [
            quoteShellArgumentsSetting,
        ];
    }
    createPathTranslatorField(containerElement) {
        new obsidian.Setting(containerElement)
            .setName("Path translator")
            .setDesc("Certain {{variables}} return file system paths, which can be converted to work in this shell. Some shells introduce sub-environments where the same file is referred to using a different absolute path than in the host operating system. A custom JavaScript function can be defined to convert absolute file paths from the host operating system's format to the one expected by the target system. Note that no directory separator changes are needed to be done - they are already changed based on the 'Shell's operating system' setting. Path translation is optional.")
            .setClass("SC-path-translator-setting")
            .addTextArea(textareaComponent => textareaComponent // TODO: Make the textarea grow based on content height.
            .setValue(this.getCustomShellConfiguration().path_translator ?? "")
            .onChange(async (newPathTranslator) => {
            if ("" === newPathTranslator.trim()) {
                // Disable translator
                this.getCustomShellConfiguration().path_translator = null;
            }
            else {
                // Enable or update translator
                this.getCustomShellConfiguration().path_translator = newPathTranslator;
            }
            await this.plugin.saveSettings();
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Path translator")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Path+translator")));
        const pathTranslatorTestVariables = [
            new Variable_VaultPath(this.plugin),
            new Variable_FilePath(this.plugin),
            new Variable_FolderPath(this.plugin),
        ];
        new obsidian.Setting(containerElement)
            .setDesc("The JavaScript code will be enclosed in a function that receives 'absolutePath' as a parameter (the file/folder path needed to be translated). As it's always absolute, the path starts from the root of the host platform's file system (" + getCurrentPlatformName() + " file system), and the function should convert it to start from the root of the sub-environment.")
            .setClass("SC-full-description")
            .addExtraButton(button => button
            .setTooltip("Test absolute path translation")
            .setIcon("type")
            .onClick(async () => {
            for (const variable of pathTranslatorTestVariables) {
                let variableArguments = {};
                if (variable instanceof Variable_FilePath || variable instanceof Variable_FolderPath) {
                    variableArguments = { mode: "absolute" };
                }
                const variableValueResult = await variable.getValue(this.getCustomShell(), null, null, variableArguments);
                const translatedPath = variableValueResult.succeeded ? variableValueResult.value : variableValueResult.error_messages[0];
                this.plugin.newNotification(variable.getFullName(false, Object.values(variableArguments)) + " = " + translatedPath);
            }
        }));
        new obsidian.Setting(containerElement)
            .setDesc("The function SHOULD NOT CAUSE side effects! It must not alter any data outside it. Try to keep the function short and simple, as possible errors are hard to inspect. The function is never called for relative paths.")
            .setClass("SC-full-description");
        createMultilineTextElement("span", `
        Examples on how {{file_path:absolute}} could be translated:
        A) From Windows path to Linux path (WSL):
        - absolutePath: C:/Obsidian/MyVault/MyFolder/MyNote.md (note that directory separators \\ are already converted to / before the function is called).
        - Expected return: /mnt/c/Obsidian/MyVault/MyFolder/MyNote.md
        B) From Windows path to Linux path (MinGW-w64):
        - absolutePath: C:/Obsidian/MyVault/MyFolder/MyNote.md (same note as above).
        - Expected return: /c/Obsidian/MyVault/MyFolder/MyNote.md
        `.trim(), new obsidian.Setting(containerElement).setClass("SC-full-description").descEl);
    }
    createShellCommandWrapperField(containerElement) {
        // Test the shell.
        const shellCommandContentVariable = new Variable_ShellCommandContent(this.plugin, ""); // Does not need a real value.
        const wrapperSettingsContainer = containerElement.createDiv({ attr: { class: "SC-setting-group" } });
        new obsidian.Setting(wrapperSettingsContainer)
            .setName("Wrapper for shell command")
            .setDesc(`Define optional preparing and/or finishing shell commands before/after an actual shell command. Can be used e.g. for adding directories to the ${getPATHEnvironmentVariableName()} environment variable, or setting character encodings. {{variables}} are supported. ${shellCommandContentVariable.getFullName(true)} must be included to denote a place for the main shell command. Can be left empty if no additional commands are needed.`)
            .setClass("SC-full-description")
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Wrapper for shell command")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Wrapper+for+shell+command")));
        const settingGroup = CreateShellCommandFieldCore(this.plugin, wrapperSettingsContainer, "", this.getCustomShellConfiguration().shell_command_wrapper ?? "", this.getCustomShell(), null, // No need to pass a TShellCommand. It would only be used for accessing variable default values in a preview text.
        this.plugin.settings.show_autocomplete_menu, async (newShellCommandWrapper) => {
            this.getCustomShellConfiguration().shell_command_wrapper = (newShellCommandWrapper === "") ? null : newShellCommandWrapper;
            await this.plugin.saveSettings();
            updateNoShellCommandContentVariableWarning();
        }, undefined, shellCommandContentVariable.getFullName(true), // Indicate that if no wrapper is defined, the shell command content is executed as-is, without additions.
        shellCommandContentVariable.getAutocompleteItems());
        settingGroup.shell_command_setting.setClass("SC-no-description");
        settingGroup.preview_setting.setClass("SC-full-description");
        new obsidian.Setting(wrapperSettingsContainer)
            .setDesc(`Use ! in ${shellCommandContentVariable.getFullName(true)} to disable escaping special characters in the shell command content. Variables used in the actual shell command are already escaped if needed, so this avoids double escaping. However, you can use the ${shellCommandContentVariable.getFullName(false)} form, if you want to e.g. store shell command content in a log file. Example: echo ${shellCommandContentVariable.getFullName(false)} >> executedShellCommands.log`)
            .setClass("SC-full-description");
        const updateNoShellCommandContentVariableWarning = () => {
            const shellCommandWrapper = this.getCustomShellConfiguration().shell_command_wrapper;
            if (null !== shellCommandWrapper && 0 === getUsedVariables(this.plugin, shellCommandWrapper, shellCommandContentVariable).size) {
                // The wrapper does not contain {{shell_command_content}}. Show a warning.
                settingGroup.preview_setting.setDesc(this.getShellCommandContentWarningText("wrapper", shellCommandContentVariable));
            }
            // Don't clear the warning by setting the preview description to "", because it might actually contain parsed
            // variables. Let just CreateShellCommandFieldCore() remove the warning when it sets its preview text to the
            // description element.
        };
        updateNoShellCommandContentVariableWarning();
    }
    createShellTestField(containerElement) {
        // Test the shell.
        containerElement.createEl("hr"); // Separate non-savable form fields visually from savable settings fields.
        const testSettingsContainer = containerElement.createDiv({ attr: { class: "SC-setting-group" } });
        new obsidian.Setting(testSettingsContainer)
            .setName("Execute a command to test the shell")
            .setDesc("This command is only executed from this settings modal. {{variables}} are supported. Output appears in a notification balloon. When playing around, keep in mind that the command is really executed, so avoid using possibly dangerous commands.")
            .setClass("SC-full-description")
            .addExtraButton(button => button
            .setTooltip("Execute the test command using this shell.")
            .setIcon("run-command")
            .onClick(async () => {
            const customShellConfiguration = this.getCustomShellConfiguration();
            if (!this.getCustomShell().getSupportedHostPlatforms().includes(getOperatingSystem())) {
                // Unsupported platform.
                this.plugin.newError("This shell is not defined to support " + getCurrentPlatformName() + ".");
                return;
            }
            if (null === customShellConfiguration.shell_command_test) {
                this.plugin.newError("The test shell command is empty.");
                return;
            }
            const wrappedShellCommandContent = customShellConfiguration.shell_command_wrapper
                ? TShellCommand.wrapShellCommandContent(this.plugin, customShellConfiguration.shell_command_test, customShellConfiguration.shell_command_wrapper, this.getCustomShell())
                : customShellConfiguration.shell_command_test // No wrapper, use unwrapped shell command content.
            ;
            const testShellCommandParsingResult = await parseVariables(this.plugin, wrappedShellCommandContent, this.getCustomShell(), true, // Enable escaping, but if this.customShellInstance.configuration.escaper is "none", then escaping is prevented anyway.
            null, // No TShellCommand, so no access for default values.
            null);
            if (testShellCommandParsingResult.succeeded) {
                // Can execute.
                const childProcess = await this.getCustomShell().spawnChildProcess(testShellCommandParsingResult.parsed_content, ShellCommandExecutor.getWorkingDirectory(this.plugin), null, // No TShellCommand is available during testing.
                null);
                if (null === childProcess) {
                    // No spawn() call was made due to some shell configuration error. Just cancel everything.
                    return;
                }
                childProcess.on("error", (error) => {
                    // Probably most errors will NOT end up here, I guess this event occurs for some rare errors.
                    this.plugin.newError("Shell test failed to execute. Error: " + error.message);
                });
                childProcess.on("exit", (exitCode) => {
                    // exitCode is null if user terminated the process. Reference: https://nodejs.org/api/child_process.html#event-exit (read on 2022-11-27).
                    // Show outputs.
                    let notified = false;
                    if (null === childProcess.stdout || null == childProcess.stderr) {
                        throw new Error("Child process's stdout and/or stderr stream is null.");
                    }
                    childProcess.stdout.setEncoding("utf8"); // Receive stdout and ...
                    childProcess.stderr.setEncoding("utf8"); // ... stderr as strings, not as Buffer objects.
                    const stdout = childProcess.stdout.read();
                    const stderr = childProcess.stderr.read();
                    if (stdout) {
                        this.plugin.newNotification(stdout);
                        notified = true;
                    }
                    if (stderr) {
                        this.plugin.newError("[" + exitCode + "]: " + stderr);
                        notified = true;
                    }
                    if (!notified) {
                        this.plugin.newNotification("Shell test finished. No output was received.");
                    }
                });
            }
            else {
                // Some variable has failed.
                this.plugin.newErrors(testShellCommandParsingResult.error_messages);
            }
        }))
            .addExtraButton(button => button
            .setIcon("help")
            .setTooltip("Documentation: Execute a command to test the shell")
            .onClick(() => gotoURL(Documentation.environments.customShells.settings + "#Execute+a+command+to+test+the+shell")));
        CreateShellCommandFieldCore(this.plugin, testSettingsContainer, "", this.getCustomShellConfiguration().shell_command_test ?? "", this.getCustomShell(), null, // No need to pass a TShellCommand. It would only be used for accessing variable default values in a preview text.
        this.plugin.settings.show_autocomplete_menu, async (newTestShellCommandContent) => {
            this.getCustomShellConfiguration().shell_command_test = (newTestShellCommandContent === "") ? null : newTestShellCommandContent;
            await this.plugin.saveSettings();
        }, undefined, "Enter a temporary shell command for testing.").shell_command_setting.setClass("SC-no-description");
    }
    getCustomShell() {
        return this.customShellInstance.getCustomShell();
    }
    getCustomShellConfiguration() {
        return this.customShellInstance.configuration;
    }
    getShellCommandContentWarningText(subject, shellCommandContentVariable) {
        return "Warning! The " + subject + " should contain " + shellCommandContentVariable.getFullName(true) + ". Otherwise, the shell will be called without the actual shell command that was supposed to be executed.";
    }
    approve() {
        if (this.onAfterApproval) {
            this.approved = true;
            this.onAfterApproval();
        }
        this.close();
    }
    onClose() {
        super.onClose();
        // Call a cancelling hook if one is defined (and if the closing happens due to cancelling, i.e. the ok button is NOT clicked).
        if (!this.approved && this.onAfterCancelling) {
            this.onAfterCancelling();
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomShellModel extends Model {
    static getSingularName() {
        return "Custom shell";
    }
    _createSettingFields(customShellInstance, containerElement) {
        debugLog("Creating setting fields for a CustomShellInstance.");
        const nameSetting = new obsidian.Setting(containerElement)
            // Configuration button
            .setName(customShellInstance.getTitle())
            .addExtraButton(button => button
            .setTooltip("Configure binary file location, escaping, directory handling etc.")
            .setIcon("gear")
            .onClick(async () => {
            await this.openSettingsModal(customShellInstance, nameSetting);
        }));
        createMultilineTextElement("span", customShellInstance.configuration.description, nameSetting.descEl);
        return nameSetting;
    }
    openSettingsModal(customShellInstance, nameSetting) {
        return new Promise((resolve) => {
            debugLog("Opening settings modal for a CustomShellInstance.");
            const modal = new CustomShellSettingsModal(this.plugin, customShellInstance, nameSetting, undefined, () => resolve(), // Resolve whether the user "approves" or "cancels".
            () => resolve());
            modal.open();
        });
    }
    defineParentConfigurationRelation(customShellInstance) {
        debugLog(`CustomShellModel: Defining parent configuration relation for CustomShellInstance ${customShellInstance.getId()}.`);
        return {
            type: "one-to-many-id",
            key: "custom_shells",
            id: customShellInstance.getId(),
        };
    }
    loadInstances(parentConfiguration) {
        debugLog(`CustomShellModel: Loading CustomShellInstances.`);
        this.customShellInstances = new CustomShellInstanceMap();
        // Iterate custom shell configurations
        let customShellConfiguration;
        for (customShellConfiguration of parentConfiguration.custom_shells) {
            this.customShellInstances.set(customShellConfiguration.id, new CustomShellInstance(this, customShellConfiguration, parentConfiguration));
        }
        return this.customShellInstances;
    }
    newInstance(parentConfiguration) {
        debugLog(`CustomShellModel: Creating a new CustomShellInstance.`);
        // Create a default configuration object
        const customShellConfiguration = this.getDefaultConfiguration();
        parentConfiguration.custom_shells.push(customShellConfiguration);
        // Create a CustomShellInstance for handling the configuration
        const customShellInstance = new CustomShellInstance(this, customShellConfiguration, parentConfiguration);
        this.customShellInstances.set(customShellConfiguration.id, customShellInstance);
        return customShellInstance;
        // TODO: Move this logic to the base Model class.
    }
    validateValue(customShellInstance, field, value) {
        debugLog(`CustomShellModel: Validating ${field} value ${value} for CustomShellInstance ${customShellInstance.getId()}.`);
        throw new Error("This method is not implemented yet."); // TODO: Check if there's some need for validation.
    }
    getDefaultConfiguration() {
        const shellCommandContentVariable = new Variable_ShellCommandContent(this.plugin, ""); // Content doesn't matter here, it's only used for getting the variable name.
        return {
            id: getIDGenerator().generateID(),
            name: "",
            description: "",
            binary_path: "",
            shell_arguments: ["-c", shellCommandContentVariable.getFullName(true)],
            host_platform: getOperatingSystem(),
            host_platform_configurations: isWindows() ? {
                win32: CustomShellModel.getDefaultHostPlatformWindowsConfiguration(),
            } : {},
            shell_platform: null,
            escaper: isWindows() ? "PowerShell" : "UnixShell",
            path_translator: null,
            shell_command_wrapper: null,
            shell_command_test: null,
        };
    }
    static getDefaultHostPlatformWindowsConfiguration() {
        return {
            quote_shell_arguments: true,
        };
    }
    augmentDeletionConfirmationModal(confirmationModal, customShellInstance) {
        const usages = customShellInstance.getUsages();
        if (usages.hasUsages()) {
            confirmationModal.extraContent.insertAdjacentText("beforeend", `If you delete the shell, the mentioned item${usages.countUsages() > 1 ? "s" : ""} will switch to use another shell.`);
        }
    }
    _deleteInstance(customShellInstance) {
        debugLog(`CustomVariableModel: Deleting CustomShellInstance ${customShellInstance.getId()}.`);
        // Remove the CustomShellInstance from all ShellCommands that use it.
        const tShellCommands = this.plugin.getTShellCommands();
        for (const shellCommandId in tShellCommands) {
            const tShellCommand = tShellCommands[shellCommandId];
            const shellCommandShells = tShellCommand.getConfiguration().shells;
            for (const platformId in shellCommandShells) {
                const shellIdentifier = shellCommandShells[platformId];
                if (customShellInstance.getId() === shellIdentifier) {
                    // This shell command uses this CustomShellInstance.
                    // Make the shell command use the default shell
                    delete shellCommandShells[platformId];
                    // Saving is done later, after the _deleteInstance() call.
                }
            }
        }
        // If any platforms use this shell by default, deselect it.
        for (const platformId of customShellInstance.getPlatformIdsUsingThisShellAsDefault()) {
            delete this.plugin.settings.default_shells[platformId];
        }
        // Delete CustomShell
        try {
            getShells().delete(customShellInstance.getCustomShell());
        }
        catch (error) {
            // If custom_variable_instance.getCustomShell() failed, no need to do anything. It just means there is no CustomShell, so there's nothing to delete.
        }
        // Delete CustomShellInstance
        this.customShellInstances.delete(customShellInstance.getId());
    }
}
class CustomShellInstanceMap extends Map {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function introduceModels(plugin) {
    debugLog("Introducing models.");
    // Keep in alphabetical order, if possible.
    introduceModelClass(new CustomShellModel(plugin));
    introduceModelClass(new CustomVariableModel(plugin));
    introduceModelClass(new PromptFieldModel(plugin));
    introduceModelClass(new PromptModel(plugin));
    introduceModelClass(new OutputWrapperModel(plugin));
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * This class serves as an accessor to CustomVariable configurations. It's paired with the CustomVariable class, which acts
 * as an operational class to implement the variable functionality.
 *
 * TODO: Decide a better name for this class. It's too easy to confuse with the CustomVariable class name.
 */
class CustomVariableInstance extends Instance {
    constructor(model, configuration, parent_configuration) {
        super(model, configuration, parent_configuration);
        this.model = model;
        this.custom_variable = null;
        // Introduce the ID to an ID generator so that it won't accidentally generate the same ID again when creating new CustomVariableInstances.
        getIDGenerator().addReservedID(configuration.id);
        debugLog(`Loaded CustomVariableInstance ${this.getID()}.`);
    }
    getID() {
        return this.configuration.id;
    }
    getFullName() {
        return `{{${this.getPrefixedName()}}}`;
    }
    /**
     * Adds an underscore in front of the name.
     */
    getPrefixedName() {
        return "_" + this.configuration.name;
    }
    getTitle() {
        return this.getFullName();
    }
    getCustomVariable() {
        if (!this.custom_variable) {
            debugLog(`CustomVariableInstance ${this.getID()}: Cannot find a CustomVariable. Maybe it's not loaded?`);
            throw new Error(this.constructor.name + ".getVariable(): Cannot find a CustomVariable. Maybe it's not loaded?");
        }
        return this.custom_variable;
    }
    createCustomVariable() {
        debugLog(`CustomVariableInstance ${this.getID()}: Creating an operational CustomVariable.`);
        this.custom_variable = new CustomVariable(this.model.plugin, this);
        this.custom_variable.onChange(async () => await this.model.plugin.updateCustomVariableViews());
        return this.custom_variable;
    }
    _getUsages() {
        const usages = new UsageContainer(this.getTitle());
        const plugin = this.model.plugin;
        const customVariableId = this.configuration.id;
        // Shell commands.
        for (const tShellCommand of plugin.getTShellCommandsAsMap().values()) {
            if (tShellCommand.getUsedCustomVariables().has(customVariableId)) {
                // The TShellCommand uses this custom variable.
                usages.addUsage({
                    title: tShellCommand.getAliasOrShellCommand(),
                }, "shellCommands");
            }
        }
        // Prompts.
        for (const prompt of plugin.getPrompts().values()) {
            if (prompt.getUsedCustomVariables().has(customVariableId)) {
                // The TShellCommand uses this custom variable.
                usages.addUsage({
                    title: prompt.getTitle(),
                }, "prompts");
            }
            // PromptFields.
            for (const promptField of prompt.prompt_fields) {
                if (promptField.getUsedCustomVariables().has(customVariableId)) {
                    usages.addUsage({
                        title: prompt.getTitle() + ": " + promptField.getTitle(),
                    }, "promptFields");
                }
            }
        }
        // Custom shells.
        for (const customShellInstance of plugin.getCustomShellInstances().values()) {
            if (customShellInstance.getUsedCustomVariables().has(customVariableId)) {
                usages.addUsage({
                    title: customShellInstance.getTitle(),
                }, "customShells");
            }
        }
        // Other {{variables}}.
        for (const variable of plugin.getVariables()) {
            let addUsage = false;
            if (variable instanceof CustomVariable) {
                // CustomVariable.
                // Don't check myself.
                if (customVariableId !== variable.getIdentifier()) {
                    if (variable.getCustomVariableInstance().getUsedCustomVariables().has(customVariableId)) {
                        // The other CustomVariable uses this CustomVariable.
                        addUsage = true;
                    }
                }
            }
            else {
                // Builtin variable.
                const defaultValueConfiguration = variable.getDefaultValueConfiguration(null);
                if (defaultValueConfiguration) {
                    const variableUsedInDefaultValueConfiguration = getUsedVariables(plugin, defaultValueConfiguration.value, plugin.getCustomVariables());
                    if (variableUsedInDefaultValueConfiguration.has(customVariableId)) {
                        // The GlobalVariableDefaultValueConfiguration uses this CustomVariable.
                        addUsage = true;
                    }
                }
            }
            if (addUsage) {
                usages.addUsage({
                    title: variable.getFullName(),
                }, "variables");
            }
        }
        // OutputWrappers.
        for (const outputWrapper of plugin.getOutputWrappers().values()) {
            if (outputWrapper.getUsedCustomVariables().has(customVariableId)) {
                usages.addUsage({
                    title: outputWrapper.getTitle(),
                }, "outputWrappers");
            }
        }
        return usages;
    }
    /**
     * Returns {{variables}} used in this {{variable}}'s default value configuration.
     *
     * @protected
     */
    _getUsedCustomVariables() {
        // Gather parseable content.
        const readVariablesFrom = [
            this.configuration.default_value?.value ?? "",
        ];
        return getUsedVariables(this.model.plugin, readVariablesFrom, this.model.plugin.getCustomVariables());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function createVariableDefaultValueFields(plugin, containerElement, targetObject) {
    // Add default value fields for each variable that can have a default value.
    for (const variable of plugin.getVariables()) {
        // Only add fields for variables that are not always accessible.
        if (!variable.isAlwaysAvailable()) {
            const setting = createVariableDefaultValueField(plugin, containerElement, variable.getFullName(), variable, targetObject);
            // Documentation link
            if (!(variable instanceof CustomVariable)) {
                setting.addExtraButton(extraButton => extraButton
                    .setIcon("help")
                    .setTooltip("Documentation: " + variable.getFullName() + " variable")
                    .onClick(() => gotoURL(variable.getDocumentationLink())));
            }
        }
    }
}
/**
 *
 * @param plugin
 * @param containerElement
 * @param settingName
 * @param variable The variable whose default value will be configured by the created setting field.
 * @param targetObject In which object's configuration the default value settings should be stored. Can be a TShellCommand or a Variable (either CustomVariable or a built-in one). If not set, the `variable` parameter will be used as a target.
 * @param onChange Called after the `type` or `value` of the default value configuration changes.
 */
function createVariableDefaultValueField(plugin, containerElement, settingName, variable, targetObject, onChange) {
    if (undefined === targetObject) {
        // No configuration target is defined, so use the variable as a target.
        targetObject = variable;
    }
    const targetType = targetObject instanceof TShellCommand
        ? 'tShellCommand'
        : targetObject instanceof CustomVariable
            ? 'customVariable'
            : 'builtinVariable';
    if ("customVariable" === targetType || "builtinVariable" === targetType) {
        if (targetObject !== variable) {
            throw new Error("If defining 'targetObject' argument as a Variable, it should be the same as the 'variable' argument.");
        }
    }
    // Get an identifier for a variable (an id, if it's a CustomVariable, otherwise the variable's name).
    const variableIdentifier = variable.getIdentifier();
    // If a default value has been defined for this variable (and this targetObject), retrieve the configuration.
    let defaultValueConfiguration;
    switch (targetType) {
        case "tShellCommand":
            defaultValueConfiguration = targetObject.getDefaultValueConfigurationForVariable(variable);
            break;
        case "builtinVariable": // Both classes have...
        case "customVariable": // ... the getGlobalDefaultValueConfiguration() method.
            defaultValueConfiguration = targetObject.getGlobalDefaultValueConfiguration();
            break;
    }
    // A function for creating configuration in onChange() callbacks if the variable does not yet have one for this configuration.
    const createDefaultValueConfiguration = () => {
        const configuration = {
            type: "show-errors",
            value: "",
        };
        // Store the configuration to the target object's configuration.
        switch (targetType) {
            case "tShellCommand":
                targetObject.getConfiguration().variable_default_values[variableIdentifier] = configuration;
                break;
            case "builtinVariable":
                if (undefined === plugin.settings.builtin_variables[variableIdentifier]) {
                    // Create a config object for this variable if it does not exist yet.
                    plugin.settings.builtin_variables[variableIdentifier] = { default_value: null };
                }
                plugin.settings.builtin_variables[variableIdentifier].default_value = configuration;
                break;
            case "customVariable":
                targetObject.getConfiguration().default_value = configuration;
                break;
        }
        return configuration;
    };
    let textareaComponent;
    // A function for updating textareaComponent visibility.
    const updateTextareaComponentVisibility = (type) => {
        if ("value" === type) {
            textareaComponent.inputEl.removeClass("SC-hide");
        }
        else {
            textareaComponent.inputEl.addClass("SC-hide");
        }
    };
    // Define a set of options for default value type
    const defaultValueTypeOptions = {
        "inherit": "",
        "show-errors": "Cancel execution and show errors",
        "cancel-silently": "Cancel execution silently",
        "value": "Execute with value:",
    };
    switch (targetType) {
        case "tShellCommand": {
            // Shell commands can have the "inherit" type.
            const globalDefaultValueConfiguration = variable.getGlobalDefaultValueConfiguration();
            const globalDefaultValueType = globalDefaultValueConfiguration ? globalDefaultValueConfiguration.type : "show-errors";
            defaultValueTypeOptions.inherit = "Inherit: " + defaultValueTypeOptions[globalDefaultValueType];
            if ("value" === globalDefaultValueType) {
                defaultValueTypeOptions.inherit += " " + globalDefaultValueConfiguration?.value;
            }
            break;
        }
        case "builtinVariable":
        case "customVariable": {
            // Variables do not have the "inherit" type.
            // @ts-ignore Don't yell about removing a non-optional property "inherit".
            delete defaultValueTypeOptions.inherit;
        }
    }
    // Create the default value setting
    let defaultValueTextareaComponent;
    const defaultValueSetting = new obsidian.Setting(containerElement)
        .setName(settingName)
        .setDesc("If not available, then:")
        .setTooltip(variable.getAvailabilityTextPlain())
        .addDropdown(dropdown => dropdown
        .addOptions(defaultValueTypeOptions)
        .setValue(defaultValueConfiguration
        ? defaultValueConfiguration.type
        : "tShellCommand" === targetType
            ? "inherit" // If configuring a TShellCommand, then default config type should be "inherit".
            : "show-errors" // If configuring a Variable, then default config type should be "show-errors", because "inherit" is not available.
    )
        .onChange(async (newType) => {
        if (!defaultValueConfiguration) {
            defaultValueConfiguration = createDefaultValueConfiguration();
        }
        // Set the new type
        defaultValueConfiguration.type = newType;
        if (targetType === "tShellCommand") {
            // Shell commands:
            if ("inherit" === newType && defaultValueConfiguration.value === "") {
                // If "inherit" is selected and no text value is typed, the configuration file can be cleaned up by removing this configuration object completely.
                // Prevent deleting, if a text value is present, because the user might want to keep it if they will later change 'type' to 'value'.
                delete targetObject.getConfiguration().variable_default_values[variableIdentifier];
            }
        }
        else {
            // Variables:
            if ("show-errors" === newType && defaultValueConfiguration.value === "") {
                // If "show-errors" is selected and no text value is typed, the configuration file can be cleaned up by removing this configuration object completely.
                // Prevent deleting, if a text value is present, because the user might want to keep it if they will later change 'type' to 'value'.
                switch (targetType) {
                    case "builtinVariable":
                        plugin.settings.builtin_variables[variableIdentifier].default_value = null;
                        break;
                    case "customVariable":
                        targetObject.getConfiguration().default_value = null;
                        break;
                }
            }
        }
        // Show/hide the textarea
        updateTextareaComponentVisibility(newType);
        // Save the settings
        await plugin.saveSettings();
        // If "Execute with value" was selected, focus on the textarea.
        if (newType === "value") {
            defaultValueTextareaComponent?.inputEl.focus();
        }
        // Extra "on change" hook.
        onChange?.();
    }))
        .addTextArea(textarea => textareaComponent = textarea
        .setValue(defaultValueConfiguration ? defaultValueConfiguration.value : "")
        .onChange(async (newValue) => {
        if (!defaultValueConfiguration) {
            defaultValueConfiguration = createDefaultValueConfiguration();
        }
        // Set the new text value
        defaultValueConfiguration.value = newValue;
        // Save the settings
        await plugin.saveSettings();
        // Extra "on change" hook.
        onChange?.();
    }).then((textareaComponent) => {
        // Autocomplete for the textarea.
        if (plugin.settings.show_autocomplete_menu) {
            createAutocomplete(plugin, textareaComponent.inputEl, () => textareaComponent.onChanged());
        }
        // Store the textarea so that the dropdown component's callback function can focus the textarea if needed.
        defaultValueTextareaComponent = textareaComponent;
    }));
    updateTextareaComponentVisibility(defaultValueConfiguration
        ? defaultValueConfiguration.type
        : targetType === "tShellCommand"
            ? "show-errors" // It does not really matter if passing "show-errors" ....
            : "inherit");
    return defaultValueSetting;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomVariableModel extends Model {
    constructor() {
        super(...arguments);
        this.custom_variable_instances = new CustomVariableInstanceMap;
    }
    static getSingularName() {
        return "Custom variable";
    }
    defineParentConfigurationRelation(custom_variable_instance) {
        debugLog(`CustomVariableModel: Defining parent configuration relation for CustomVariableInstance ${custom_variable_instance.getID()}.`);
        return {
            type: "one-to-many-id",
            key: "custom_variables",
            id: custom_variable_instance.getID(),
        };
    }
    loadInstances(parent_configuration) {
        debugLog(`CustomVariableModel: Loading CustomVariableInstances.`);
        this.custom_variable_instances.clear();
        parent_configuration.custom_variables.forEach((custom_variable_configuration) => {
            this.custom_variable_instances.set(custom_variable_configuration.id, new CustomVariableInstance(this, custom_variable_configuration, parent_configuration));
        });
        return this.custom_variable_instances;
    }
    newInstance(parent_configuration) {
        debugLog(`CustomVariableModel: Creating a new CustomVariableInstance.`);
        // Create a default configuration object
        const custom_variable_configuration = this.getDefaultConfiguration();
        parent_configuration.custom_variables.push(custom_variable_configuration);
        // Create a CustomVariableInstance for handling the configuration
        const custom_variable_instance = new CustomVariableInstance(this, custom_variable_configuration, parent_configuration);
        this.custom_variable_instances.set(custom_variable_configuration.id, custom_variable_instance);
        // Create an operational variable.
        this.plugin.getVariables().add(custom_variable_instance.createCustomVariable());
        return custom_variable_instance;
        // TODO: Move this logic to the base Model class.
    }
    _createSettingFields(instance, container_element) {
        debugLog(`CustomVariableModel: Creating setting fields for CustomVariableInstance ${instance.getID()}.`);
        // Make the fields appear closer together.
        container_element.addClass("SC-setting-group");
        // Heading setting
        const heading_setting = new obsidian.Setting(container_element)
            .setName(instance.getFullName())
            .setHeading();
        // Name setting
        new obsidian.Setting(container_element)
            .setName("Variable name")
            .setDesc("Must contain at least one character. Allowed characters are letters a-z, numbers 0-9 and an underscore _")
            .setClass("SC-custom-variable-name-setting")
            .addText(text => text
            .setValue(instance.configuration.name)
            .onChange((new_name) => {
            // TODO: Find a way to create this kind of trivial onChange() functions in the Model base class.
            instance.setIfValid("name", new_name).then(async () => {
                // Valid
                heading_setting.setName(instance.getFullName()); // Also removes a possible warning message.
                instance.getCustomVariable().updateProperties(); // Update the name also to the operational variable, not only in configuration.
                await this.plugin.saveSettings();
                await this.plugin.updateCustomVariableViews();
            }, (reason) => {
                // Not valid
                if (typeof reason === "string") {
                    // This is a validation error message.
                    // Display a warning message.
                    heading_setting.setName(reason + " The name was not saved.");
                }
                else {
                    // Some other runtime error has occurred.
                    throw reason;
                }
            });
        }));
        // Description setting
        new obsidian.Setting(container_element)
            .setName("Description")
            .setDesc("Appears in autocomplete lists along with the variable name, and also in the 'Custom variables' pane, if you use it.")
            .addText(text => text
            .setValue(instance.configuration.description)
            .onChange(async (new_description) => {
            // TODO: Find a way to create this kind of trivial onChange() functions in the Model base class.
            instance.configuration.description = new_description;
            instance.getCustomVariable().updateProperties(); // Update the description also to the operational variable, not only in configuration.
            await this.plugin.saveSettings();
            await this.plugin.updateCustomVariableViews();
        }));
        // Default value setting
        createVariableDefaultValueField(this.plugin, container_element, "Default value", instance.getCustomVariable(), undefined, () => {
            // After changing the default value settings, update values in 'Custom variables' view.
            this.plugin.updateCustomVariableViews().then();
        });
        // Usage listing.
        const usageListContainerElement = document.createElement("small");
        usageListContainerElement.appendChild(instance.getUsages().toHTMLElement("short"));
        container_element.appendChild(usageListContainerElement);
        return heading_setting;
    }
    validateValue(custom_variable_instance, field, custom_variable_name) {
        debugLog(`CustomVariableModel: Validating ${field} value ${custom_variable_name} for CustomVariableInstance ${custom_variable_instance.getID()}.`);
        return new Promise((resolve, reject) => {
            switch (field) {
                case "name":
                    // Check that the name contains only characters a-z, 0-9 and/or underline _
                    if (!custom_variable_name.match(CustomVariable.getCustomVariableValidNameRegex(false))) {
                        // Incorrect format.
                        reject(`The name {{_${custom_variable_name}}} does not meet the naming requirements.`);
                        return;
                    }
                    // Check if the name is a duplicate.
                    if (this.isCustomVariableNameDuplicate(custom_variable_name, custom_variable_instance)) {
                        // It's a duplicate.
                        reject(`The name {{_${custom_variable_name}}} is already reserved.`);
                    }
                    else {
                        // It's unique.
                        resolve();
                    }
                    return;
                default:
                    // Other fields do not need validation.
                    resolve();
                    return;
            }
        });
    }
    getDefaultConfiguration() {
        // Generate a unique name for the variable by using a sequential number.
        let sequential_number = 1;
        while (this.isCustomVariableNameDuplicate(String(sequential_number))) {
            sequential_number++;
        }
        // Create a configuration object.
        return {
            id: getIDGenerator().generateID(),
            name: String(sequential_number),
            description: "",
            default_value: null,
        };
    }
    async _deleteInstance(custom_variable_instance) {
        debugLog(`CustomVariableModel: Deleting CustomVariableInstance ${custom_variable_instance.getID()}.`);
        // Remove the CustomVariableInstance from all PromptFields that use it.
        for (const prompt of this.plugin.getPrompts().values()) {
            for (const prompt_field of prompt.prompt_fields) {
                if (custom_variable_instance.getID() === prompt_field.configuration.target_variable_id) {
                    // This prompt field uses this CustomVariableInstance.
                    // Remove the variable from use.
                    prompt_field.configuration.target_variable_id = "";
                    // Saving is done later, after the _deleteInstance() call.
                }
            }
        }
        // Delete CustomVariable
        try {
            this.plugin.getVariables().delete(custom_variable_instance.getCustomVariable());
        }
        catch (error) {
            // If custom_variable_instance.getCustomVariable() failed, no need to do anything. It just means there is no CustomVariable, so there's nothing to delete.
        }
        // Delete CustomVariableInstance
        this.custom_variable_instances.delete(custom_variable_instance.getID());
        // remove the variable from custom variable side panes.
        await this.plugin.updateCustomVariableViews();
    }
    /**
     * Can be changed to public if needed.
     */
    isCustomVariableNameDuplicate(custom_variable_name, ignore_custom_variable_instance) {
        let is_duplicate = false;
        this.custom_variable_instances.forEach((custom_variable2_instance, custom_variable_id) => {
            // First check can the current custom variable attend to the duplicate test.
            if (ignore_custom_variable_instance && custom_variable_id === ignore_custom_variable_instance.getID()) {
                // Don't check this instance. This skipping is used for the current owner of the name.
                return;
            }
            // Now do the actual duplicate test.
            if (custom_variable_name.toLocaleLowerCase() === custom_variable2_instance.configuration.name.toLocaleLowerCase()) {
                is_duplicate = true;
            }
        });
        return is_duplicate;
    }
}
class CustomVariableInstanceMap extends Map {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomVariableSettingsModal extends SC_Modal {
    constructor(plugin, custom_variable_instance, on_after_creation, on_after_cancelling) {
        super(plugin);
        this.custom_variable_instance = custom_variable_instance;
        this.on_after_creation = on_after_creation;
        this.on_after_cancelling = on_after_cancelling;
        this.created = false;
    }
    onOpen() {
        super.onOpen();
        const model = getModel(CustomVariableModel.name);
        model.createSettingFields(this.custom_variable_instance, this.modalEl, false);
        new obsidian.Setting(this.modalEl)
            .addButton(button => button
            .setButtonText("Create")
            .onClick(() => this.approve()));
    }
    approve() {
        this.created = true;
        this.on_after_creation();
        this.close();
    }
    onClose() {
        super.onClose();
        if (!this.created) {
            this.on_after_cancelling();
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomVariableView extends obsidian.ItemView {
    constructor(plugin, leaf) {
        super(leaf);
        this.plugin = plugin;
    }
    getDisplayText() {
        return "Shell commands: Custom variables"; // Same as getDisplayTextStatic().
    }
    static getDisplayTextStatic() {
        return "Shell commands: Custom variables"; // Same as getDisplayText().
    }
    getViewType() {
        return CustomVariableView.ViewType;
    }
    getIcon() {
        return "code-glyph"; // Same as getIconStatic().
    }
    static getIconStatic() {
        return "code-glyph"; // Same as getIcon().
    }
    async onOpen() {
        this.container_element = this.containerEl.children[1].createDiv(); // I don't know why I cannot create elements directly under this.containerEl (they wouldn't show up). I did the same thing as was done here: https://marcus.se.net/obsidian-plugin-docs/guides/custom-views (referenced 2022-03-23).
        this.container_element.addClass("container");
        await this.updateContent();
    }
    async updateContent() {
        this.container_element.empty();
        this.container_element.createEl("h3", { text: "Custom variables" });
        const variableListElement = this.container_element.createEl("ul");
        for (const customVariableInstance of this.plugin.getCustomVariableInstances().values()) {
            let customVariableValue = customVariableInstance.getCustomVariable().getCustomVariableValue();
            let customVariableState = null;
            if (!customVariableInstance.getCustomVariable().hasOwnValue()) {
                // The variable has no value yet.
                if ("value" === customVariableInstance.configuration.default_value?.type) {
                    // Indicate that the variable has a default value defined, which will practically be used as long as no overriding value is set.
                    if ("" === customVariableInstance.configuration.default_value.value.trim()) {
                        customVariableState = "No value yet, but defaults to: An empty text.";
                    }
                    else {
                        customVariableState = "No value yet, but defaults to: "; // The value will appear next to the state text later below.
                        customVariableValue = customVariableInstance.configuration.default_value.value;
                    }
                }
                else {
                    // No default value is defined, so no value is accessible.
                    customVariableState = "No value yet.";
                }
            }
            else if ("" === customVariableValue) {
                customVariableState = "An empty text.";
            }
            const variableListItemElement = variableListElement.createEl("li", {
                text: customVariableInstance.getFullName(),
                attr: {
                    "aria-label": customVariableInstance.configuration.description,
                    "class": "SC-custom-variable-view-list-item",
                },
            });
            variableListItemElement.createEl("br");
            if (null !== customVariableState) {
                createMultilineTextElement("em", customVariableState, variableListItemElement);
            }
            if (null !== customVariableValue) {
                if (customVariableInstance.getCustomVariable().shouldCloak()) {
                    // Display a cloaked password.
                    variableListItemElement.insertAdjacentHTML("beforeend", cloakPassword(customVariableValue));
                }
                else {
                    // Bold normal values to make them more prominent in contrast to variable names and "No value yet."/"An empty text." texts.
                    createMultilineTextElement("strong", customVariableValue, variableListItemElement);
                }
            }
        }
    }
}
CustomVariableView.ViewType = "SC-custom-variables-view";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Preaction {
    constructor(plugin, configuration, t_shell_command) {
        this.plugin = plugin;
        this.configuration = configuration;
        this.t_shell_command = t_shell_command;
    }
    /**
     * Maybe this wrapper method is unneeded, but have it for a while at least.
     */
    perform(parsing_process, sc_event) {
        return this.doPreaction(parsing_process, sc_event);
    }
    /**
     * Returns variables that are dependent of this Preaction, i.e. variables whose value is set by this Preaction.
     * If a variable is READ by a Preaction, it is NOT considered to be _dependent_ of the Preaction, as long as the variable's
     * value is not changed by the Preaction.
     *
     * By default, it returns an empty VariableSet, because not all Preactions will use variables at all.
     */
    getDependentVariables() {
        return new VariableSet();
    }
}
function createPreaction(plugin, preaction_configuration, t_shell_command) {
    switch (preaction_configuration.type) {
        case "prompt":
            return new Preaction_Prompt(plugin, preaction_configuration, t_shell_command);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Preaction_Prompt extends Preaction {
    constructor(plugin, configuration, t_shell_command) {
        super(plugin, configuration, t_shell_command);
        this.configuration = configuration;
    }
    doPreaction(parsing_process, sc_event) {
        // TODO: Now that doPreaction() returns a similar Promise as is received from openPrompt(), consider just returning the same Promise instead of creating a new one.
        return new Promise((resolve) => {
            this.getPrompt().openPrompt(this.t_shell_command, parsing_process, sc_event).then((execution_confirmed) => {
                // The PromptModal has been closed.
                // Check if user wanted to execute the shell command or cancel.
                if (execution_confirmed) {
                    // User wants to execute.
                    resolve(true);
                }
                else {
                    // User wants to cancel.
                    resolve(false);
                }
            });
        });
    }
    /**
     * Returns all the CustomVariables whose values this Preaction's Prompt sets.
     */
    getDependentVariables() {
        const variables = new VariableSet();
        for (const prompt_field of this.getPrompt().prompt_fields) {
            // Check that the PromptField has a target variable defined. Otherwise getTargetVariable() would cause a crash.
            if ("" !== prompt_field.configuration.target_variable_id) {
                variables.add(prompt_field.getTargetVariable());
            }
        }
        return variables;
    }
    /**
     * TODO: Remove.
     */
    getDefaultConfiguration() {
        return {
            type: "prompt",
            enabled: false,
            prompt_id: "",
        };
    }
    getPrompt() {
        const promptId = this.configuration.prompt_id;
        if (undefined === promptId) {
            throw new Error("Prompt id is undefined in configuration.");
        }
        const prompt = this.plugin.getPrompts().get(promptId);
        if (undefined === prompt) {
            throw new Error("Prompt with id '" + promptId + "' does not exist");
        }
        return prompt;
    }
}
function getDefaultPreaction_Prompt_Configuration() {
    return {
        type: "prompt",
        enabled: false,
        prompt_id: "",
    };
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class PromptField extends Instance {
    constructor(model, prompt, configuration, prompt_field_index) {
        super(model, configuration, prompt.configuration);
        this.model = model;
        this.prompt = prompt;
        this.configuration = configuration;
        this.prompt_field_index = prompt_field_index;
        this.parsing_errors = [];
    }
    /**
     *
     * @param container_element
     * @param t_shell_command
     * @param sc_event Used when parsing variables for default_value and the inputted value. Needed so that also {{event_*}} variables can be used in prompts.
     */
    async createField(container_element, t_shell_command, sc_event) {
        // Parse variables in common properties.
        const shell = this.getShell(t_shell_command);
        const label_parsing_result = await parseVariables(this.prompt.model.plugin, this.configuration.label, shell, false, t_shell_command, sc_event);
        const description_parsing_result = await parseVariables(this.prompt.model.plugin, this.configuration.description, shell, false, t_shell_command, sc_event);
        // Create a base for the field.
        const setting = new obsidian.Setting(container_element)
            .setName(label_parsing_result.succeeded ? label_parsing_result.parsed_content : label_parsing_result.original_content)
            .setDesc(description_parsing_result.succeeded ? description_parsing_result.parsed_content : description_parsing_result.original_content);
        // Create a type specific input field.
        const on_change = () => this.valueHasChanged(t_shell_command, sc_event);
        await this.createTypeSpecificField(setting, on_change, t_shell_command, sc_event);
        // Set up onFocus hook.
        let inputElement;
        switch (this.configuration.type) {
            case "single-line-text":
            case "multi-line-text":
            case "password":
                inputElement = this.fieldComponent.inputEl;
                break;
            case "toggle":
                inputElement = this.fieldComponent.toggleEl;
                break;
            case "single-choice":
                inputElement = this.fieldComponent.selectEl;
                break;
        }
        inputElement.onfocus = () => this.hasGottenFocus();
        // Create a preview setting element. It will not contain any actual setting elements, just text.
        this.preview_setting = new obsidian.Setting(container_element);
        // Parse variables in the default value and insert it to the field.
        // Note that this is a different "default value" than what TShellCommand considers as variables' default values! This is about a _field's_ default value, not a variable's default value. t_shell_command is passed in order to allow any possible variables in the field's default value to access the variables' default values (which come from TShellCommand).
        await this.applyDefaultValue(t_shell_command, sc_event);
    }
    async createTypeSpecificField(setting, onChange, tShellCommand, scEvent) {
        const plugin = this.prompt.model.plugin;
        // Create the field
        switch (this.configuration.type) {
            case "single-line-text":
            case "password": {
                const isPassword = this.configuration.type === "password";
                setting.addText((text_component) => {
                    this.fieldComponent = text_component;
                    text_component.onChange(onChange);
                    if (isPassword) {
                        text_component.inputEl.type = "password";
                        if (DEBUG_ON) {
                            // Warn about logging passwords.
                            setting.descEl.insertAdjacentHTML("beforebegin", "<strong>SC debug mode is on!</strong> Any passwords entered will be logged to console (" + CmdOrCtrl() + " + " + (process.platform === "darwin" ? "Option" : "Shift") + " + I) when variables are parsed. <strong>Do not enter important passwords when the debug mode is on!</strong>");
                        }
                        text_component.setPlaceholder("Passwords are handled without encryption.");
                    }
                });
                // Show autocomplete menu (if enabled) - but not for password field. (An autocomplete might reveal what was typed in the field, and passwords are probably not stored in autocomplete lists.)
                if (plugin.settings.show_autocomplete_menu && !isPassword) {
                    const input_element = setting.controlEl.find("input");
                    createAutocomplete(plugin, input_element, onChange);
                }
                break;
            }
            case "multi-line-text": {
                setting.addTextArea((textAreaComponent) => {
                    this.fieldComponent = textAreaComponent;
                    decorateMultilineField(this.model.plugin, textAreaComponent, onChange, 2);
                });
                // Show autocomplete menu (if enabled).
                if (plugin.settings.show_autocomplete_menu) {
                    const textAreaElement = setting.controlEl.find("textarea");
                    createAutocomplete(plugin, textAreaElement, onChange);
                }
                break;
            }
            case "toggle":
                setting.addToggle((toggleComponent) => {
                    this.fieldComponent = toggleComponent;
                    toggleComponent.onChange(onChange);
                });
                break;
            case "single-choice": {
                // Scaffold a list of dropdown options
                const dropdownOptions = {};
                for (const choice of this.configuration.choices) {
                    let choiceValue;
                    let choiceLabel;
                    if (Array.isArray(choice)) {
                        // Different value and label.
                        [choiceValue, choiceLabel] = choice;
                    }
                    else {
                        // Unified value and label.
                        choiceValue = choiceLabel = choice;
                    }
                    // Parse variables in choiceValue.
                    const choiceValueParsingResult = await parseVariables(this.prompt.model.plugin, choiceValue, this.getShell(tShellCommand), false, tShellCommand, scEvent);
                    if (choiceValueParsingResult.succeeded) {
                        choiceValue = choiceValueParsingResult.parsed_content;
                    }
                    // Parse variables in choiceLabel.
                    const choiceLabelParsingResult = await parseVariables(this.prompt.model.plugin, choiceLabel, this.getShell(tShellCommand), false, tShellCommand, scEvent);
                    if (choiceLabelParsingResult.succeeded) {
                        choiceLabel = choiceLabelParsingResult.parsed_content;
                    }
                    // Add to dropdown options.
                    dropdownOptions[choiceValue] = choiceLabel;
                }
                // Create the dropdown.
                setting.addDropdown((dropdownComponent) => {
                    this.fieldComponent = dropdownComponent;
                    dropdownComponent.addOptions(dropdownOptions);
                    dropdownComponent.onChange(onChange);
                });
                break;
            }
            default:
                // @ts-ignore Do not yell when the switch covers all type cases. Ignores this error: TS2339: Property 'type' does not exist on type 'never'.
                throw new Error("Unidentified PromptField type: " + this.configuration.type);
        }
    }
    getTitle() {
        return this.configuration.label === "" ? "Unlabelled field" : this.configuration.label;
    }
    /**
     * Gets a value from the form field.
     * @protected
     */
    getValue() {
        switch (this.configuration.type) {
            case "single-line-text":
            case "multi-line-text":
            case "password":
            case "single-choice":
                return this.fieldComponent.getValue();
            case "toggle": {
                const toggledOn = this.fieldComponent.getValue();
                return toggledOn ? this.configuration.on_result : this.configuration.off_result;
            }
        }
    }
    /**
     * Sets a value to the form field.
     * @param value
     * @param tShellCommand Used for variable parsing if the field is a toggle.
     * @param scEvent Used for variable parsing if the field is a toggle.
     * @protected
     */
    async setValue(value, tShellCommand, scEvent) {
        switch (this.configuration.type) {
            case "single-line-text":
            case "multi-line-text":
            case "password":
                this.fieldComponent.setValue(value);
                break;
            case "toggle": {
                // Parse variables in onResult.
                let onResult = this.configuration.on_result;
                const onResultParsingResult = await parseVariables(this.model.plugin, onResult, this.getShell(tShellCommand), false, tShellCommand, scEvent);
                if (onResultParsingResult.succeeded) {
                    onResult = onResultParsingResult.parsed_content;
                }
                // Translate the value to a boolean state suitable for a toggle:
                // - If the value equals on_result -> Toggle is ON.
                // - If the value equals off_result -> Toggle is OFF.
                // - If the value equals neither on_result nor off_result -> Toggle is OFF.
                // - The comparison is NOT case-sensitive.
                const toggledOn = value.toLocaleLowerCase() === onResult.toLocaleLowerCase();
                this.fieldComponent.setValue(toggledOn);
                break;
            }
            case "single-choice": {
                const dropdownComponent = this.fieldComponent;
                let valueExistsInChoices = false;
                for (const optionElement of Array.from(dropdownComponent.selectEl.options)) {
                    const choiceValue = optionElement.value;
                    if (choiceValue.toLocaleLowerCase() === value.toLocaleLowerCase()) {
                        // The given value exists as an option. Select it.
                        dropdownComponent.setValue(choiceValue);
                        valueExistsInChoices = true;
                    }
                }
                if (!valueExistsInChoices) {
                    // The given value does not exist. Select the first option.
                    const firstOptionElement = dropdownComponent.selectEl.options.item(0);
                    if (null !== firstOptionElement) {
                        dropdownComponent.setValue(firstOptionElement.value);
                    }
                }
                break;
            }
            default:
                // @ts-ignore Do not yell when the switch covers all type cases. Ignores this error: TS2339: Property 'type' does not exist on type 'never'.
                throw new Error("Unidentified PromptField type: " + this.configuration.type);
        }
    }
    /**
     * Parses the default value and sets it to the form element.
     * @param t_shell_command
     * @param sc_event
     * @private
     */
    async applyDefaultValue(t_shell_command, sc_event) {
        const default_value = this.configuration.default_value;
        const parsing_result = await parseVariables(this.prompt.model.plugin, default_value, this.getShell(t_shell_command), false, t_shell_command, sc_event);
        if (!parsing_result.succeeded) {
            // Parsing failed.
            await this.setValue(default_value, t_shell_command, sc_event); // Use the unparsed value. If default value contains a variable that cannot be parsed, a user can see the variable in the prompt modal and either fix it or change it to something else.
        }
        else {
            // Parsing succeeded.
            await this.setValue(parsing_result.parsed_content, t_shell_command, sc_event);
        }
        await this.valueHasChanged(t_shell_command, sc_event);
    }
    getParsedValue() {
        return this.parsed_value;
    }
    /**
     * Tries to get a parsed value, but if it's not available (probably due to incorrect usage of variables), returns an
     * unparsed value instead().
     */
    getParsedOrRawValue() {
        return this.parsed_value ?? this.getValue();
    }
    getParsingErrors() {
        return this.parsing_errors;
    }
    /**
     * Updates this.parsed_value, this.parsing_errors and this.preview_setting .
     *
     * @param t_shell_command
     * @param sc_event
     * @private
     */
    async valueHasChanged(t_shell_command, sc_event) {
        let preview = "";
        const dontParseFieldTypes = [
            "single-choice",
            "password", // Passwords are static.
        ];
        const doParseVariables = !dontParseFieldTypes.contains(this.configuration.type);
        if (doParseVariables) {
            // Parse variables in the value.
            const parsing_result = await parseVariables(this.prompt.model.plugin, this.getValue(), this.getShell(t_shell_command), false, t_shell_command, sc_event);
            if (!parsing_result.succeeded) {
                // Parsing failed.
                this.parsed_value = null;
                if (parsing_result.error_messages.length > 0) {
                    // Display the first error message. If there are more, others can be omitted.
                    preview = parsing_result.error_messages[0];
                }
                else {
                    // If there are no error messages, then errors are silently ignored by user's variable configuration, in which case just show the original content.
                    preview = parsing_result.original_content;
                }
                this.parsing_errors = parsing_result.error_messages;
            }
            else {
                // Parsing succeeded
                this.parsed_value = parsing_result.parsed_content;
                preview = parsing_result.parsed_content;
                this.parsing_errors = []; // No errors.
            }
            if (0 === parsing_result.count_parsed_variables) {
                // If no variables were used, hide the description as it's not needed to repeat the value that already shows up in the form field.
                preview = "";
            }
        }
        else {
            // No need to parse variables, as they are already parsed before.
            this.parsed_value = this.getValue();
            this.parsing_errors = []; // This probably is already an empty array, but just make sure.
        }
        // Update the preview element.
        this.preview_setting.setDesc(preview);
        // Call a possible external callback
        if (this.on_change_callback) {
            this.on_change_callback();
        }
    }
    /**
     * @param on_change_callback A callback that will be called whenever the field's value is changed.
     */
    onChange(on_change_callback) {
        this.on_change_callback = on_change_callback;
    }
    /**
     * @param on_focus_callback A callback that will be called whenever the field is focused.
     */
    onFocus(on_focus_callback) {
        this.on_focus_callback = on_focus_callback;
    }
    /**
     * Should be called by the subclass when the field has gotten focus.
     */
    hasGottenFocus() {
        if (this.on_focus_callback) {
            this.on_focus_callback(this);
        }
    }
    /**
     * Forces focus on the field.
     */
    setFocus() {
        switch (this.configuration.type) {
            case "single-line-text":
            case "multi-line-text":
            case "password":
                this.fieldComponent.inputEl.focus();
                break;
            case "toggle": {
                this.fieldComponent.toggleEl.focus();
                break;
            }
            case "single-choice":
                this.fieldComponent.selectEl.focus();
                break;
            default:
                // @ts-ignore Do not yell when the switch covers all type cases. Ignores this error: TS2339: Property 'type' does not exist on type 'never'.
                throw new Error("Unidentified PromptField type: " + this.configuration.type);
        }
    }
    /**
     * Ensures the field is correctly set up. If it's not, a Prompt cannot be opened.
     * Performed checks:
     *  - The field must have a target variable defined.
     *  - A dropdown field has less than two choices.
     *
     * @return True when valid, a string error message when not valid.
     */
    isConfigurationValid() {
        // Check that target variable is defined.
        if (!this.configuration.target_variable_id) {
            return `Field '${this.getTitle()}' does not have a target variable.`;
        }
        else {
            try {
                this.getTargetVariableInstance(); // Just try to get a CustomVariableInstance. No need to use it here, but if this fails, we know the variable is removed.
            }
            catch (error) {
                return `Field '${this.getTitle()}' uses a target variable which does not exist anymore.`;
            }
        }
        // Type specific checks.
        switch (this.configuration.type) {
            case "single-choice":
                if (Object.getOwnPropertyNames(this.configuration.choices).length < 2) { // FIXME: The check does not work, it always turns out as valid.
                    return `Dropdown field '${this.getTitle()}' must have at least two options.`;
                }
        }
        // All ok.
        return true;
    }
    /**
     * Ensures that the field is filled, if it's mandatory. If the field is not mandatory, it's always valid.
     *
     * @return True when valid, false when not valid.
     */
    validate() {
        if (!this.configuration.required) {
            // No need to validate, because the field is not mandatory.
            return true;
        }
        // Ensure the field is filled
        return this.isFilled();
    }
    isFilled() {
        switch (this.configuration.type) {
            case "single-line-text":
            case "multi-line-text":
            case "password":
            case "single-choice": // Dropdown can also use the length logic - nothing is selected if the selected option's value is an empty string.
                return this.getValue().length > 0;
            case "toggle":
                // Consider a toggle filled when it's checked.
                return this.fieldComponent.getValue();
        }
    }
    getTargetVariableInstance() {
        const target_variable_id = this.configuration.target_variable_id;
        const custom_variable_instance = this.prompt.model.plugin.getCustomVariableInstances().get(target_variable_id);
        if (!custom_variable_instance) {
            throw new Error(this.constructor.name + ".getTargetVariableInstance(): CustomVariableInstance with ID '" + target_variable_id + "' was not found");
        }
        return custom_variable_instance;
    }
    getTargetVariable() {
        const custom_variable_instance = this.getTargetVariableInstance();
        return custom_variable_instance.getCustomVariable();
    }
    getShell(tShellCommand) {
        if (tShellCommand) {
            return tShellCommand.getShell();
        }
        // If no shell command is available (= preview mode), use just whatever global default is defined. It's just a preview, so it's enough to have at least some shell.
        return this.prompt.model.plugin.getDefaultShell();
    }
    /**
     * Returns {{variables}} used in this PromptField's label, description, and default value.
     *
     * @protected
     */
    _getUsedCustomVariables() {
        // Gather parseable content.
        const readVariablesFrom = [
            this.configuration.label,
            this.configuration.description,
            this.configuration.default_value,
        ];
        const usedCustomVariables = getUsedVariables(this.model.plugin, readVariablesFrom, this.model.plugin.getCustomVariables());
        // Add target variable, if defined.
        if (this.configuration.target_variable_id) {
            usedCustomVariables.set(this.configuration.target_variable_id, this.getTargetVariable());
        }
        return usedCustomVariables;
    }
    /**
     * Iterates all PromptField configuration properties and fills in possibly missing ones. This is needed after changing
     * field type.
     */
    ensureAllConfigurationPropertiesExist() {
        const defaultConfiguration = this.model.getDefaultConfiguration(this.configuration.type);
        ensureObjectHasProperties(this.configuration, defaultConfiguration);
    }
    /**
     * Removes configuration properties that are not used by the current field type. Exception: a surplus property is
     * not removed, if its value differs from the property's default value.
     */
    removeSurplusConfigurationProperties() {
        const defaultConfiguration = this.model.getDefaultConfiguration(this.configuration.type);
        const surplusProperties = getObjectSurplusProperties(this.configuration, defaultConfiguration);
        const combinedDefaultConfigurations = this.model.combineAllDefaultConfigurations();
        let propertyName;
        for (propertyName in surplusProperties) {
            if (deepEqual(this.configuration[propertyName], combinedDefaultConfigurations[propertyName])) {
                // This property's value is the same as the default value, and it does not belong to the current field type's configuration, so it can be removed.
                delete this.configuration[propertyName];
            }
            // If the value is different from the default value, then it's wise to keep the surplus property, so that if user changes back to a field type that uses the property, they will not lose the value.
        }
    }
}
const PromptFieldTypes = {
    "single-line-text": "Single line text",
    "multi-line-text": "Multiline text",
    "toggle": "Toggle",
    "single-choice": "Dropdown menu",
    "password": "Password",
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class PromptFieldModel extends Model {
    static getSingularName() {
        return "Prompt field";
    }
    defineParentConfigurationRelation(prompt_field) {
        return {
            type: "one-to-many-index",
            key: "fields",
            index: prompt_field.prompt_field_index,
        };
    }
    loadInstances(prompt) {
        const prompt_fields = new PromptFieldSet;
        prompt.configuration.fields.forEach((field_configuration, index) => {
            prompt_fields.add(this.createInstance(prompt, field_configuration, index));
        });
        return prompt_fields;
    }
    newInstance(prompt) {
        // TODO: Move this logic to the base Model class.
        // Setup a default configuration
        const prompt_field_configuration = this.getDefaultConfiguration();
        // Instantiate a PromptField
        const prompt_field = this.createInstance(prompt, prompt_field_configuration, prompt.configuration.fields.length);
        // Store the configuration into the prompt's configuration
        prompt.configuration.fields.push(prompt_field_configuration);
        // Store the PromptField instance into its parent Prompt's list of fields.
        prompt.prompt_fields.add(prompt_field);
        // Return the PromptField
        return prompt_field;
    }
    createInstance(prompt, prompt_field_configuration, prompt_field_index) {
        return new PromptField(this, prompt, prompt_field_configuration, prompt_field_index);
    }
    _createSettingFields(prompt_field, container_element) {
        const label_placeholders = [
            "What is your name?",
            "How big is the universe?",
            "How long is eternity?",
            "What is your lucky number?",
            "What is your favorite song?",
            "What is your favorite color?",
            "How many books have you read?",
            "What is the purpose of life?",
        ];
        const default_value_placeholders = [
            ["Bond, James Bond", "John Doe", "Jane Doe", "Mr. Bean"],
            ["Very big, and still expanding", "93 billion light-years"],
            ["Infinite", "Too long to wait for"],
            [String(randomInteger(0, 9)), "I don't have one"],
            ["We are the world (USA for Africa)", "Heal the world (Michael Jackson)", "Imagine (John Lennon)", "Circle of life (Elton John)"],
            ["Blue as deep as an ocean", "Red as love", "Grass-green", "Snow-white"],
            ["Thousands", "Many", "Countless", "None"],
            ["Thinking", "Being a being", "42"],
        ];
        const label_placeholder_index = randomInteger(0, label_placeholders.length - 1);
        const default_value_placeholders_subset = default_value_placeholders[label_placeholder_index];
        // Create a list of custom variables
        const custom_variable_options = {};
        this.plugin.getCustomVariableInstances().forEach((custom_variable_instance, custom_variable_id) => {
            custom_variable_options[custom_variable_id] = custom_variable_instance.getFullName();
        });
        const on_default_value_setting_change = async (new_default_value) => {
            prompt_field.configuration.default_value = new_default_value;
            // There are two default value fields. One is hidden, one visible, but both need to be kept up-to-date (in case user changes prompt field type).
            // It's not known here, which field provided the new value here, so assign the value to both fields.
            const singleLineDefaultValueField = setting_group.default_value_setting.controlEl.find("input");
            const multiLineDefaultValueField = setting_group.default_value_setting.controlEl.find("textarea");
            singleLineDefaultValueField.value = new_default_value;
            multiLineDefaultValueField.value = new_default_value;
            await this.plugin.saveSettings();
        };
        const showCorrectDefaultValueField = (show) => {
            const singleLineDefaultValueField = setting_group.default_value_setting.controlEl.find("input");
            const multiLineDefaultValueField = setting_group.default_value_setting.controlEl.find("textarea");
            if (show === "single-line") {
                singleLineDefaultValueField.style.display = "block";
                multiLineDefaultValueField.style.display = "none";
            }
            else {
                singleLineDefaultValueField.style.display = "none";
                multiLineDefaultValueField.style.display = "block";
            }
        };
        // Create the setting fields
        const setting_group_element = container_element.createDiv({ attr: { class: "SC-setting-group" } });
        let label_setting_component;
        let description_setting_component;
        const defaultValueFieldPlaceholder = prompt_field.configuration.label
            ? "" // If the label is defined, do not add a placeholder here, as the label's placeholder is not visible, so this placeholder would not make sense.
            : default_value_placeholders_subset[randomInteger(0, default_value_placeholders_subset.length - 1)];
        const setting_group = {
            heading_setting: new obsidian.Setting(setting_group_element)
                .setName("") // This will be set down below.
                .setHeading()
                .addDropdown(dropdownComponent => dropdownComponent
                .addOptions(PromptFieldTypes)
                .setValue(prompt_field.configuration.type)
                .onChange(async (newType) => {
                // Change the PromptField's type.
                prompt_field.configuration.type = newType;
                // Declare possibly new configuration properties.
                prompt_field.ensureAllConfigurationPropertiesExist();
                // Remove unneeded configuration properties defined by other field types.
                prompt_field.removeSurplusConfigurationProperties();
                // Create possible new setting fields.
                this.createTypeSpecificSettingFields(prompt_field, typeSpecificSettingFieldsContainer, showCorrectDefaultValueField);
                await this.plugin.saveSettings();
            })),
            label_setting: new obsidian.Setting(setting_group_element)
                .setName("Field label")
                .addText(text => label_setting_component = text
                .setValue(prompt_field.configuration.label)
                .setPlaceholder(label_placeholders[label_placeholder_index])
                .onChange(async (new_label) => {
                prompt_field.configuration.label = new_label;
                _update_heading();
                await this.plugin.saveSettings();
            })),
            default_value_setting: new obsidian.Setting(setting_group_element)
                .setName("Default value")
                .addTextArea(textAreaComponent => {
                textAreaComponent
                    .setValue(prompt_field.configuration.default_value)
                    .setPlaceholder(defaultValueFieldPlaceholder);
                decorateMultilineField(this.plugin, textAreaComponent, on_default_value_setting_change, 2);
            })
                .addText(textComponent => textComponent // Only visible for other field types.
                .setValue(prompt_field.configuration.default_value)
                .setPlaceholder(defaultValueFieldPlaceholder)
                .onChange(on_default_value_setting_change)),
            description_setting: new obsidian.Setting(setting_group_element)
                .setName("Description")
                .addText(text => description_setting_component = text
                .setValue(prompt_field.configuration.description)
                .onChange(async (new_description) => {
                prompt_field.configuration.description = new_description;
                await this.plugin.saveSettings();
            })),
            target_variable_setting: new obsidian.Setting(setting_group_element)
                .setName("Target variable")
                .setDesc("Where the inputted value will be stored in. You can use the variable in a shell command.")
                .addDropdown(dropdown => dropdown
                .addOption("", "") // An option for a situation when nothing is selected.
                .addOptions(custom_variable_options)
                .addOption("new", "Create a new custom variable")
                .setValue(prompt_field.configuration.target_variable_id)
                .onChange((new_target_variable_id) => {
                if ("new" === new_target_variable_id) {
                    // Create a new custom variable.
                    const model = getModel(CustomVariableModel.name);
                    const custom_variable_instance = model.newInstance(this.plugin.settings);
                    this.plugin.saveSettings().then(() => {
                        const modal = new CustomVariableSettingsModal(this.plugin, custom_variable_instance, async () => {
                            // Variable is created.
                            dropdown.addOption(custom_variable_instance.getID(), custom_variable_instance.getTitle());
                            dropdown.setValue(custom_variable_instance.getID());
                            prompt_field.configuration.target_variable_id = custom_variable_instance.getID();
                            await this.plugin.saveSettings();
                        }, async () => {
                            dropdown.setValue(prompt_field.configuration.target_variable_id); // Reset the dropdown selection.
                            // Variable creation was cancelled.
                            model.deleteInstance(custom_variable_instance);
                            await this.plugin.saveSettings();
                        });
                        modal.open();
                    });
                }
                else {
                    // Use an existing target variable (or an empty id "").
                    // Check that this target variable is not reserved.
                    prompt_field.setIfValid("target_variable_id", new_target_variable_id).then(async () => {
                        // It can be used.
                        await this.plugin.saveSettings();
                    }, (error_message) => {
                        if (typeof error_message === "string") {
                            // This is a validation error message.
                            // The target variable is reserved.
                            dropdown.setValue(prompt_field.configuration.target_variable_id); // Reset the dropdown selection.
                            this.plugin.newNotification(error_message);
                        }
                        else {
                            // Some other runtime error has occurred.
                            throw error_message;
                        }
                    });
                }
            })),
            required_setting: new obsidian.Setting(setting_group_element)
                .setName("Is required")
                .setDesc("If on, the field needs to be filled before the prompt can be submitted.")
                .addToggle(toggle => toggle
                .setValue(prompt_field.configuration.required)
                .onChange(async (new_required) => {
                prompt_field.configuration.required = new_required;
                await this.plugin.saveSettings();
            })),
        };
        _update_heading();
        function _update_heading() {
            setting_group.heading_setting.setName(prompt_field.getTitle());
        }
        // Autocomplete menu
        if (this.plugin.settings.show_autocomplete_menu) {
            // Show autocomplete menu (= a list of available variables).
            const label_input_element = setting_group.label_setting.controlEl.find("input");
            createAutocomplete(this.plugin, label_input_element, () => label_setting_component.onChanged());
            const default_value_input_element = setting_group.default_value_setting.controlEl.find("input");
            createAutocomplete(this.plugin, default_value_input_element, on_default_value_setting_change);
            const defaultValueTextareaElement = setting_group.default_value_setting.controlEl.find("textarea");
            createAutocomplete(this.plugin, defaultValueTextareaElement, on_default_value_setting_change);
            const description_input_element = setting_group.description_setting.controlEl.find("input");
            createAutocomplete(this.plugin, description_input_element, () => description_setting_component.onChanged());
        }
        const typeSpecificSettingFieldsContainer = setting_group_element.createDiv();
        this.createTypeSpecificSettingFields(prompt_field, typeSpecificSettingFieldsContainer, showCorrectDefaultValueField);
        return setting_group.heading_setting;
    }
    createTypeSpecificSettingFields(promptField, containerElement, showCorrectDefaultValueField) {
        // Remove possibly existing setting fields, in case this method is recalled.
        containerElement.innerHTML = "";
        const promptFieldConfiguration = promptField.configuration;
        switch (promptFieldConfiguration.type) {
            case "single-line-text":
            case "multi-line-text":
                // No special settings for these types.
                break;
            case "toggle":
                // Toggled on result.
                new obsidian.Setting(containerElement)
                    .setName("Result when toggled on")
                    .setDesc("What value the target variable will have if the toggle is checked. The toggle is checked by default, if the Default value (defined above) matches this value. The match is not case-sensitive.")
                    .addText(textComponent => textComponent
                    .setValue(promptFieldConfiguration.on_result)
                    .onChange(async (onResult) => {
                    promptFieldConfiguration.on_result = onResult;
                    await this.plugin.saveSettings();
                })
                    .then((textComponent) => createAutocomplete(this.plugin, textComponent.inputEl, () => textComponent.onChanged())));
                // Toggled off result.
                new obsidian.Setting(containerElement)
                    .setName("Result when toggled off")
                    .setDesc("What value the target variable will have if the toggle is not checked. {{variables}} can be used both here and above.")
                    .addText(textComponent => textComponent
                    .setValue(promptFieldConfiguration.off_result)
                    .onChange(async (offResult) => {
                    promptFieldConfiguration.off_result = offResult;
                    await this.plugin.saveSettings();
                })
                    .then((textComponent) => createAutocomplete(this.plugin, textComponent.inputEl, () => textComponent.onChanged())));
                break;
            case "single-choice": {
                const choices = promptFieldConfiguration.choices.map((choice) => {
                    if (Array.isArray(choice)) {
                        // Different value and label.
                        return choice[0] + "|" + choice[1];
                    }
                    else {
                        // Unified value and label.
                        return choice;
                    }
                }).join("\n");
                new obsidian.Setting(containerElement)
                    .setName("Choices")
                    .setDesc("A list of options of which one can be selected. Put each option on their own line. You can define a separate value and label for an option by separating them with a pipe |, e.g. MyValue|MyLabel. {{variables}} are supported in both values and labels.")
                    .addTextArea((textAreaComponent) => {
                    textAreaComponent.setValue(choices);
                    textAreaComponent.inputEl.rows = 10;
                    createAutocomplete(this.plugin, textAreaComponent.inputEl, () => textAreaComponent.onChanged());
                    decorateMultilineField(this.plugin, textAreaComponent, async (newChoices) => {
                        // Save changed choices.
                        promptFieldConfiguration.choices = [];
                        for (const choice of newChoices.split("\n")) {
                            if (choice.contains("|")) {
                                // The option defines a separate value and label.
                                const choiceParts = choice.split("|");
                                const choiceValue = choiceParts.shift();
                                const choiceLabel = choiceParts.join("|"); // Re-join. If the option line contained multiple colons, then only the first one splits, and the rest will be used in the label as-is.
                                promptFieldConfiguration.choices.push([choiceValue, choiceLabel]);
                            }
                            else {
                                // The option defines a unified value and label.
                                promptFieldConfiguration.choices.push(choice);
                            }
                        }
                        await this.plugin.saveSettings();
                    }, 3);
                });
                break;
            }
            case "password": {
                const warningSetting = new obsidian.Setting(containerElement);
                createCallout(warningSetting.descEl, "warning", "Passwords are handled without encryption", "The password field only cloaks its value visually. Internally, the SC plugin handles passwords without any encryption or other security means. The SC plugin's developer does not have security expertise, and is uncertain if other plugins or third-party applications could theoretically read the entered values.");
                break;
            }
            // This field type does not need extra setting fields.
        }
        // Single line or multiline default value field?
        showCorrectDefaultValueField(promptFieldConfiguration.type === "multi-line-text" ? "multi-line" : "single-line");
    }
    validateValue(prompt_field, field, value) {
        switch (field) {
            case "target_variable_id": {
                const new_target_variable_id = value; // A more descriptive name for 'value'.
                // Always allow an empty target_variable_id. A Prompt cannot be opened if a field lacks a target_variable_id, but it's allowed to be stored in the configuration, because new Prompts cannot have a default selected target variable.
                if ("" === new_target_variable_id) {
                    return Promise.resolve();
                }
                // Check that the target variable is not used by other fields of the same Prompt.
                for (const other_prompt_field of prompt_field.prompt.prompt_fields) {
                    if (prompt_field !== other_prompt_field) { // Do not check the same field. Only check other fields.
                        // Check if this other field has the same target variable.
                        if (new_target_variable_id === other_prompt_field.configuration.target_variable_id) {
                            // They have the same target_variable_id.
                            // Return an error message.
                            const targetVariableInstance = this.plugin.getCustomVariableInstances().get(new_target_variable_id);
                            if (undefined === targetVariableInstance) {
                                throw new Error("Could not find target variable with id " + new_target_variable_id);
                            }
                            const target_variable_name = targetVariableInstance.getFullName();
                            return Promise.reject(`Target variable ${target_variable_name} is already used by another field in the same prompt. Select another variable.`);
                        }
                    }
                }
                // All fields have been checked and no collisions were found.
                return Promise.resolve();
            }
            default: {
                // No validation for other fields.
                throw new Error(this.constructor.name + ".validateValue(): No validation is implemented for other fields.");
            }
        }
    }
    getDefaultConfiguration(fieldType = "single-line-text") {
        const commonProperties = {
            label: "",
            description: "",
            default_value: "",
            target_variable_id: "",
            required: true,
        };
        switch (fieldType) {
            case "single-line-text":
            case "password":
                return {
                    type: fieldType,
                    ...commonProperties,
                    // TODO: Implement placeholder property later.
                };
            case "multi-line-text":
                return {
                    type: fieldType,
                    ...commonProperties,
                    // TODO: Implement placeholder property later.
                };
            case "toggle":
                return {
                    type: fieldType,
                    ...commonProperties,
                    on_result: "ON",
                    off_result: "OFF",
                };
            case "single-choice":
                return {
                    type: fieldType,
                    ...commonProperties,
                    choices: [
                        "",
                    ],
                };
        }
    }
    /**
     * Combines all prompt field types' default configurations into a single object.
     *
     * @return {Object} The combined default configurations.
     */
    combineAllDefaultConfigurations() {
        const defaultConfigurations = [];
        for (const promptFieldType of Object.getOwnPropertyNames(PromptFieldTypes)) {
            defaultConfigurations.push(this.getDefaultConfiguration(promptFieldType));
        }
        return combineObjects(...defaultConfigurations);
    }
    _deleteInstance(prompt_field) {
        prompt_field.prompt.prompt_fields.delete(prompt_field);
    }
}
class PromptFieldSet extends Set {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class Prompt extends Instance {
    constructor(model, plugin, configuration, parent_configuration) {
        super(model, configuration, parent_configuration);
        this.model = model;
        this.plugin = plugin;
        this.configuration = configuration;
        this.parent_configuration = parent_configuration;
        this.prompt_fields = new PromptFieldSet();
        // Introduce the ID to an ID generator so that it won't accidentally generate the same ID again when creating new Prompts.
        getIDGenerator().addReservedID(configuration.id);
        this.createFields();
    }
    getID() {
        return this.configuration.id;
    }
    getTitle() {
        return this.configuration.title;
    }
    getConfiguration() {
        return this.configuration;
    }
    getCSSClass() {
        return Prompt.getCSSBaseClass() + "-" + this.getID();
    }
    static getCSSBaseClass() {
        return "SC-prompt-modal";
    }
    getCSSClasses() {
        return [
            Prompt.getCSSBaseClass(),
            this.getCSSClass(),
        ];
    }
    /**
     * @param t_shell_command Can be null, if wanted to just preview the Prompt modal without really executing a shell command. Inputted values will still be assigned to target variables.
     * @param parsing_process
     * @param sc_event
     * @return Promise The boolean value tells whether the user wants to execute a shell command (true) or cancel (false).
     */
    openPrompt(t_shell_command, parsing_process, sc_event) {
        const can_open_prompt_result = this.canOpenPrompt();
        if (true !== can_open_prompt_result) {
            // Some error is preventing opening the prompt.
            // A human-readable error message is contained in can_open_prompt_result.
            debugLog("Could not open Prompt " + this.getID() + " because of error: " + can_open_prompt_result);
            this.plugin.newError(can_open_prompt_result);
            return Promise.resolve(false); // false: Cancel execution (pretends that a user cancelled it, but it's ok).
        }
        debugLog("Opening Prompt " + this.getID());
        const modal = new PromptModal(this.plugin, this.prompt_fields, t_shell_command, parsing_process, this, sc_event, () => { return this.validateFields(); });
        modal.open();
        return modal.promise;
    }
    canOpenPrompt() {
        // Check that all PromptFields have no configuration errors.
        for (const prompt_field of this.prompt_fields) {
            const validity = prompt_field.isConfigurationValid();
            if ("string" === typeof validity) {
                // An error is detected.
                return `Cannot open prompt '${this.getTitle()}': ${validity}`;
            }
        }
        // All ok.
        return true;
    }
    /**
     * Creates PromptField instances, NOT setting fields!
     */
    createFields() {
        debugLog("Creating fields for Prompt " + this.getID());
        const prompt_field_model = getModel(PromptFieldModel.name);
        this.prompt_fields = prompt_field_model.loadInstances(this);
    }
    /**
     * Validates values in PromptField instances, NOT setting fields!
     */
    validateFields() {
        debugLog("Validating fields for Prompt " + this.getID());
        // Iterate all fields and check their validity.
        const error_messages = [];
        this.prompt_fields.forEach((prompt_field) => {
            // Check if the field has parsing errors.
            const parsing_errors = prompt_field.getParsingErrors();
            for (const parsing_error of parsing_errors) {
                // This field has parsing error(s).
                error_messages.push(`'${prompt_field.getTitle()}': ` + parsing_error);
            }
            // Check other validity.
            if (!prompt_field.validate()) {
                // This field failed to validate.
                // TODO: Change this so that the message will come from prompt_field.validate().
                error_messages.push(`'${prompt_field.getTitle()}' needs to be filled.`);
            }
        });
        // Return the result.
        if (0 === error_messages.length) {
            return Promise.resolve();
        }
        else {
            return Promise.reject(error_messages);
        }
    }
    getFieldByVariable(variable) {
        for (const field of this.prompt_fields) {
            if (field.configuration.target_variable_id === variable.getConfiguration().id) {
                return field;
            }
        }
        return null;
    }
    hasFieldOfType(promptFieldType) {
        for (const field of this.prompt_fields) {
            if (field.configuration.type === promptFieldType) {
                return true;
            }
        }
        return false;
    }
    /**
     * When previewing a PromptModal, there is no real shell command available (because no shell command has triggered the
     * PromptModal). This method creates just a dummy shell command string that imitates a command that would echo variable values.
     */
    getExampleShellCommand() {
        const variable_names = [];
        for (const prompt_field of this.prompt_fields) {
            variable_names.push(prompt_field.getTargetVariableInstance().getFullName());
        }
        return "echo " + variable_names.join(" ");
    }
    _getUsages() {
        const usages = new UsageContainer(this.getTitle());
        for (const tShellCommand of this.plugin.getTShellCommandsAsMap().values()) {
            const promptIdsUsedByShellCommand = Object.values(tShellCommand.getConfiguration().preactions).map((preactionConfiguration) => preactionConfiguration?.prompt_id);
            if (promptIdsUsedByShellCommand.contains(this.getID())) {
                usages.addUsage({
                    title: tShellCommand.getAliasOrShellCommand(),
                }, "shellCommands");
            }
        }
        return usages;
    }
    /**
     * Returns {{variables}} used in this Prompt's title, description and execution button.
     *
     * @protected
     */
    _getUsedCustomVariables() {
        // Gather parseable content.
        const readVariablesFrom = [
            this.configuration.title,
            this.configuration.description,
            this.configuration.execute_button_text,
        ];
        return getUsedVariables(this.plugin, readVariablesFrom, this.plugin.getCustomVariables());
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class PromptModal extends SC_Modal {
    constructor(plugin, prompt_fields, 
    /** Can be null, if wanted to just preview the Prompt modal without really executing a shell command. Inputted values will still be assigned to target variables. */
    t_shell_command, parsing_process, prompt, sc_event, 
    /** A function that is called when a user clicks the execution button. This function should check the form elements' validity and return false if there are unfilled fields. */
    validator) {
        super(plugin);
        this.prompt_fields = prompt_fields;
        this.t_shell_command = t_shell_command;
        this.parsing_process = parsing_process;
        this.prompt = prompt;
        this.sc_event = sc_event;
        this.validator = validator;
        this.user_confirmed_ok = false;
        this.promise = new Promise((resolve) => {
            this.resolve_promise = resolve;
        });
    }
    async onOpen() {
        super.onOpen();
        // Parse and display title
        const title_parsing_result = await parseVariables(this.plugin, this.prompt.getTitle(), this.getShell(), false, this.t_shell_command, this.sc_event);
        this.setTitle(title_parsing_result.succeeded
            ? title_parsing_result.parsed_content
            : title_parsing_result.original_content);
        // Parse and display description
        if (this.prompt.configuration.description) {
            const description_parsing_result = await parseVariables(this.plugin, this.prompt.configuration.description, this.getShell(), false, this.t_shell_command, this.sc_event);
            const description = description_parsing_result.succeeded
                ? description_parsing_result.parsed_content
                : description_parsing_result.original_content;
            const description_element = createMultilineTextElement("p", description, this.modalEl);
            description_element.addClass("setting-item-description"); // A CSS class defined by Obsidian.
        }
        // Preview the shell command (if wanted)
        // TODO: Extract to a separate method, as this is a big block of code.
        let update_shell_command_preview = null; // Stays null if .preview_shell_command is false.
        let focused_prompt_field;
        if (this.prompt.getConfiguration().preview_shell_command) {
            let shell_command_preview_text;
            if (this.t_shell_command?.getAlias()) {
                this.modalEl.createEl("p", { text: this.t_shell_command.getAlias(), attr: { class: "SC-no-margin" } });
            }
            // Create "Show variable values" toggle
            let preview_variable_values = true;
            const variable_names_visible_icon = "code-glyph";
            const variable_values_visible_glyph = "price-tag-glyph";
            const preview_variable_values_setting = new obsidian.Setting(this.modalEl)
                .addExtraButton(button => button
                .setIcon(variable_values_visible_glyph)
                .setTooltip("Toggle showing variable names or values.")
                .onClick(() => {
                preview_variable_values = !preview_variable_values;
                button.setIcon(preview_variable_values
                    ? variable_values_visible_glyph
                    : variable_names_visible_icon);
                if (null === update_shell_command_preview) {
                    throw new Error("Toggle showing variable names or value: update_shell_command_preview function is not defined.");
                }
                update_shell_command_preview();
            }));
            // Notify about passwords.
            if (this.prompt.hasFieldOfType("password")) {
                this.modalEl.createEl("p", { text: "(Passwords are cloaked in the preview.)", attr: { class: "SC-no-margin SC-small-font" } });
            }
            // Decide what text to use in the preview
            const shellCommandParsingResult = this.parsing_process?.getParsingResults().shellCommandContent; // Use unwrapped shell command content, as that's shorter to display, and wrappers probably don't contain so interesting content.
            if (shellCommandParsingResult?.succeeded) {
                // Show a real shell command. Use preparsed content (= content that might have some variables already parsed).
                shell_command_preview_text = shellCommandParsingResult.parsed_content; // as string = if shellCommandParsingResult?.succeeded is true, then .parsed_content is always string.
            }
            else if (this.t_shell_command) {
                // Show a real shell command. No preparsed content is available. This content does not have any variables parsed yet.
                shell_command_preview_text = this.t_shell_command.getShellCommandContent(); // ForPreview() is good, no need to show Shell augmentations as they would just clutter up the preview.
            }
            else {
                // Make up a fake "shell command" for previewing.
                shell_command_preview_text = this.prompt.getExampleShellCommand();
                this.modalEl.createEl("p", { text: "(This is not a real shell command, it's just an example for this preview when no real shell command is available.)", attr: { class: "SC-no-margin SC-small-font" } });
            }
            this.modalEl.createEl("hr");
            // A function for handling preview text updates.
            update_shell_command_preview = async () => {
                let shell_command_preview_text_final = shell_command_preview_text;
                if (preview_variable_values) {
                    // The preview should show the VALUES.
                    // Ensure the form fields do not contain any parsing errors. (If there are errors, an unparsed preview text will be shown).
                    if (this.getPromptFieldsParsingErrors().length === 0) {
                        // All fields are parsed ok (= individual parsing).
                        // Insert the field values into the shell command preview by parsing the preview text.
                        // Get current values from the prompt fields.
                        const fresh_values = this.getPromptFieldsValues(); // These PromptField values are fresh, so not yet stored in the actual variables.
                        // Parse variables in the shell command preview text.
                        const parsing_result = await parseVariables(this.plugin, shell_command_preview_text, this.getShell(), true, this.t_shell_command, this.sc_event, undefined, // Use all variables.
                        (variable, raw_value) => {
                            if (fresh_values.has(variable.variable_name)) {
                                // Change the value to the one in the prompt field.
                                raw_value.error_messages = []; // Remove any possible error messages.
                                raw_value.succeeded = true; // This needs to reflect that there are no error messages.
                                raw_value.value = fresh_values.get(variable.variable_name); // It's always a string because fresh_values.has() is used above.
                            }
                            // No modifications.
                        }, (variable, escapedValue, originalValue) => {
                            // Is the value a password?
                            let useValue;
                            const isPassword = (variable instanceof CustomVariable)
                                ? this.prompt.getFieldByVariable(variable)?.configuration.type === "password" ?? false
                                : false;
                            if (isPassword) {
                                // Cloak a password.
                                useValue = cloakPassword(originalValue);
                            }
                            else {
                                useValue = escapedValue;
                            }
                            // Emphasize the value that came from the currently focused field.
                            if (focused_prompt_field) {
                                if (variable.variable_name.toLocaleLowerCase() === focused_prompt_field.getTargetVariableInstance().getPrefixedName().toLocaleLowerCase()) {
                                    // Make the value bold.
                                    return `<strong>${useValue}</strong>`;
                                }
                            }
                            // No modifications.
                            return useValue;
                        });
                        if (parsing_result.succeeded) {
                            shell_command_preview_text_final = parsing_result.parsed_content;
                        }
                    }
                }
                else {
                    // The preview should show the VARIABLE NAMES.
                    if (focused_prompt_field) {
                        const pattern = new RegExp(focused_prompt_field.getTargetVariable().getPattern(), "igu"); // i: case-insensitive; g: match all occurrences instead of just the first one. u: support 4-byte unicode characters too.
                        shell_command_preview_text_final = shell_command_preview_text_final.replace(pattern, (replaceable_variable_name) => {
                            return "<strong>" + replaceable_variable_name + "</strong>";
                        });
                    }
                }
                preview_variable_values_setting.descEl.innerHTML = shell_command_preview_text_final;
            };
        }
        // Create fields
        let is_first_field = true;
        for (const prompt_field of this.prompt_fields) {
            await prompt_field.createField(this.modalEl.createDiv({ attr: { class: "SC-setting-group" } }), this.t_shell_command, this.sc_event);
            if (update_shell_command_preview) {
                prompt_field.onChange(update_shell_command_preview);
            }
            prompt_field.onFocus((prompt_field) => {
                focused_prompt_field = prompt_field;
                if (update_shell_command_preview) {
                    update_shell_command_preview();
                }
            });
            if (is_first_field) {
                // Focus on the first field.
                is_first_field = false;
                prompt_field.setFocus();
            }
        }
        if (update_shell_command_preview) {
            // Set a preview text. Must be done after fields are created, because their values are accessed.
            update_shell_command_preview();
        }
        // Tip about variables
        let tip = "";
        if (this.prompt_fields.size > 0) {
            // TODO: When implementing different field types, add a check that the tip is only shown when there are text/numeric fields present.
            // Only show the tip if this modal actually contains fields. Prompts can also be used as custom 'confirmation prompts' without any fields.
            tip = "Tip! You can use {{variables}} in text fields.";
        }
        // Execute button
        const execute_button_text_parsing_result = await parseVariables(this.plugin, this.prompt.configuration.execute_button_text, this.getShell(), false, this.t_shell_command, this.sc_event);
        const execute_button_text = execute_button_text_parsing_result.succeeded
            ? execute_button_text_parsing_result.parsed_content
            : execute_button_text_parsing_result.original_content;
        new obsidian.Setting(this.modalEl)
            .setDesc(tip)
            .addButton(button => button
            .setButtonText(execute_button_text)
            .onClick(() => this.approve()));
        if (!this.t_shell_command) {
            // Notice that this is a preview only Prompt.
            this.modalEl.createEl("p", { text: `This is a preview prompt. No shell command will be executed, but clicking the '${this.prompt.configuration.execute_button_text}' button will still store the inputted value(s) to variable(s).` }).addClass("SC-prompt-dry-run-notice");
        }
        // Add CSS classes so that custom styling can be done on a per-prompt modal basis (or for all prompt modals via a common class).
        this.modalEl.addClasses(this.prompt.getCSSClasses());
    }
    approve() {
        this.validator().then(async () => {
            // The form fields are filled ok
            await this.assignValuesToVariables();
            this.resolve_promise(true);
            this.user_confirmed_ok = true;
            this.close();
        }, (error_messages) => {
            if (Array.isArray(error_messages)) {
                // There were some problems with the fields.
                this.plugin.newErrors(error_messages);
            }
            else {
                // Some other runtime error has occurred.
                throw error_messages;
            }
        });
    }
    onClose() {
        super.onClose();
        if (!this.user_confirmed_ok) { // TODO: Find out if there is a way to not use this kind of flag property. Can the status be checked from the promise itself?
            this.resolve_promise(false);
        }
    }
    async assignValuesToVariables() {
        let promptField;
        for (promptField of this.prompt_fields) {
            const cloak = "password" === promptField.configuration.type;
            await promptField.getTargetVariable().setValue(promptField.getParsedOrRawValue(), "manual", cloak);
        }
    }
    /**
     * Gathers a Map of variable values typed in the form, but does not store the values into the variables. Called when
     * generating a preview text, so the result of this method will not persist in any way.
     * @private
     */
    getPromptFieldsValues() {
        const values = new Map();
        for (const prompt_field of this.prompt_fields) {
            values.set(prompt_field.getTargetVariable().variable_name, prompt_field.getParsedValue() ?? ""); // TODO: Should getParsedValue() ?? "" be changed to getParsedOrRawValue(), too?
        }
        return values;
    }
    getPromptFieldsParsingErrors() {
        const parsing_errors = [];
        for (const prompt_field of this.prompt_fields) {
            parsing_errors.push(...prompt_field.getParsingErrors());
        }
        return parsing_errors;
    }
    getShell() {
        if (this.t_shell_command) {
            // This is a real usage of the PromptModal, so a TShellCommand is available. Look up the shell from that.
            return this.t_shell_command.getShell();
        }
        else {
            // Just trying the PromptModal. Just use some shell for variable escaping in an example preview.
            return this.plugin.getDefaultShell();
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class PromptModel extends Model {
    constructor() {
        super(...arguments);
        this.prompts = new PromptMap();
    }
    static getSingularName() {
        return "Prompt";
    }
    defineParentConfigurationRelation(prompt) {
        return {
            type: "one-to-many-id",
            key: "prompts",
            id: prompt.getID(),
        };
    }
    loadInstances(parent_configuration) {
        debugLog("Loading Prompt instances.");
        this.prompts = new PromptMap();
        parent_configuration.prompts.forEach((prompt_configuration) => {
            const prompt = new Prompt(this, this.plugin, prompt_configuration, parent_configuration);
            this.prompts.set(prompt_configuration.id, prompt);
        });
        return this.prompts;
    }
    newInstance(parent_configuration) {
        debugLog("Creating a new Prompt instance.");
        // TODO: Move this logic to the base Model class.
        // Setup a default configuration and generate an ID
        const prompt_configuration = this.getDefaultConfiguration();
        // Instantiate a Prompt
        const prompt = new Prompt(this, this.plugin, prompt_configuration, this.plugin.settings);
        this.prompts.set(prompt.getID(), prompt);
        // Store the configuration into plugin's settings
        this.plugin.settings.prompts.push(prompt_configuration);
        // Return the Prompt
        return prompt;
    }
    _createSettingFields(prompt, container_element) {
        debugLog("Creating setting fields for a Prompt instance.");
        const prompt_name_setting = new obsidian.Setting(container_element)
            // Configuration button
            .setName(prompt.getTitle())
            .addExtraButton(button => button
            .setTooltip("Define prompt fields")
            .setIcon("gear")
            .onClick(() => {
            this.openSettingsModal(prompt, prompt_name_setting);
        }));
        // TODO: Add Prompt description here. Make sure it supports multiline, take a look at CustomShellModel._createSettingFields().
        return prompt_name_setting;
    }
    validateValue(prompt, field, value) {
        // This method is not used, so it can just resolve all the time.
        return Promise.resolve(undefined);
    }
    openSettingsModal(prompt, prompt_name_setting) {
        debugLog("Opening settings modal for a Prompt instance.");
        const modal = new PromptSettingsModal(this.plugin, prompt, prompt_name_setting);
        modal.open();
    }
    getDefaultConfiguration() {
        return {
            id: getIDGenerator().generateID(),
            title: "",
            description: "",
            preview_shell_command: false,
            fields: [],
            execute_button_text: "Execute",
        };
    }
    _deleteInstance(prompt) {
        debugLog("Deleting a Prompt instance.");
        // Remove the Prompt from all TShellCommands that use it.
        const shell_commands = this.plugin.getTShellCommands();
        for (const shell_command_id in shell_commands) {
            const t_shell_command = shell_commands[shell_command_id];
            for (const preaction_configuration of t_shell_command.getConfiguration().preactions) {
                if ("prompt" === preaction_configuration.type) {
                    const preaction_prompt_configuration = preaction_configuration;
                    if (prompt.getID() === preaction_prompt_configuration.prompt_id) {
                        // This TShellCommand uses this Prompt.
                        // Remove the Prompt from use.
                        preaction_prompt_configuration.enabled = false;
                        preaction_prompt_configuration.prompt_id = undefined;
                        t_shell_command.resetPreactions();
                        // Saving is done later, after the _deleteInstance() call.
                    }
                }
            }
        }
        this.prompts.delete(prompt.getID());
    }
}
class PromptMap extends Map {
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * @param plugin
 * @param model_class_name
 * @param button_container_element
 * @param instance_container_element
 * @param parent_instance_or_configuration
 * @param onClick Gets called if a user clicks the button. Will receive the newly created instance and its main Setting field.
 */
function createNewModelInstanceButton(plugin, model_class_name, button_container_element, instance_container_element, parent_instance_or_configuration, onClick) {
    debugLog("Creating a button for creating a new instance for model " + model_class_name + ".");
    const model = getModel(model_class_name);
    new obsidian.Setting(button_container_element)
        .addButton(button => button
        .setButtonText("New " + model.static().getSingularName().toLocaleLowerCase())
        .onClick(async () => {
        if (null === parent_instance_or_configuration) {
            throw new Error("createNewModelInstanceButton(): Parent instance or configuration is null.");
        }
        const instance = model.newInstance(parent_instance_or_configuration);
        const main_setting = model.createSettingFields(instance, instance_container_element);
        onClick?.(instance, main_setting);
        await plugin.saveSettings();
    }));
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class PromptSettingsModal extends SC_Modal {
    constructor(plugin, prompt, 
    /** Can be undefined if the modal is created from a place where there is no name element. */
    prompt_name_setting, 
    /** If defined, a button will be added and on_after_approval() / on_after_cancelling() will be called depending on whether the button was clicked or not. */
    ok_button_text, on_after_approval, on_after_cancelling) {
        super(plugin);
        this.prompt = prompt;
        this.prompt_name_setting = prompt_name_setting;
        this.ok_button_text = ok_button_text;
        this.on_after_approval = on_after_approval;
        this.on_after_cancelling = on_after_cancelling;
        this.approved = false;
    }
    onOpen() {
        super.onOpen();
        const container_element = this.modalEl;
        const title_and_description_group_element = container_element.createDiv({ attr: { class: "SC-setting-group" } });
        // Title
        const title_setting = new obsidian.Setting(title_and_description_group_element)
            .setName("Prompt title")
            .addExtraButton(icon => icon
            .setTooltip("Try the prompt without executing any shell command.")
            .setIcon("run-command")
            .onClick(() => {
            // "Dry run" the Prompt
            this.prompt.openPrompt(null, null, null).then();
        }))
            .addText(text => text
            .setValue(this.prompt.getTitle())
            .onChange(async (new_title) => {
            this.prompt.getConfiguration().title = new_title;
            await this.plugin.saveSettings();
            // Update the title in a name setting. (Only if the modal was created from a place where a Prompt name element exists).
            this.prompt_name_setting?.setName(new_title);
        })
            .then((title_setting_component) => {
            // Autocomplete for Title.
            if (this.plugin.settings.show_autocomplete_menu) {
                createAutocomplete(this.plugin, title_setting_component.inputEl, () => title_setting_component.onChanged());
            }
        }));
        const title_input_element = title_setting.controlEl.find("input");
        // Focus on the title field.
        title_input_element.focus();
        // Description
        new obsidian.Setting(title_and_description_group_element)
            .setName("Description")
            .setDesc("Displayed between the prompt title and fields. Both Description and Title support {{variables}}.")
            .addTextArea(textarea => textarea
            .setValue(this.prompt.configuration.description)
            .onChange(async (new_description) => {
            this.prompt.getConfiguration().description = new_description;
            await this.plugin.saveSettings();
        })
            .then((description_component) => {
            // Autocomplete for Description.
            if (this.plugin.settings.show_autocomplete_menu) {
                createAutocomplete(this.plugin, description_component.inputEl, () => description_component.onChanged());
            }
        }));
        // Preview shell command
        new obsidian.Setting(container_element)
            .setName("Preview shell command in prompt")
            .setDesc("If this is on, the prompt will display the executable shell command with variable names in it, and highlight the variable(s) that will be affected by the values inputted in the prompt.")
            .addToggle(toggle => toggle
            .setValue(this.prompt.getConfiguration().preview_shell_command)
            .onChange(async (new_value) => {
            this.prompt.getConfiguration().preview_shell_command = new_value;
            await this.plugin.saveSettings();
        }));
        // Fields
        new obsidian.Setting(container_element)
            .setName("Fields")
            .setDesc("Tip! You can use {{variables}} in 'Field label', 'Default value', 'Description' etc.");
        const prompt_field_model = getModel(PromptFieldModel.name);
        const fields_container = container_element.createDiv();
        this.prompt.prompt_fields.forEach((prompt_field) => {
            prompt_field_model.createSettingFields(prompt_field, fields_container);
        });
        // New field button
        createNewModelInstanceButton(this.plugin, PromptFieldModel.name, container_element, fields_container, this.prompt);
        // Execute button text
        new obsidian.Setting(container_element.createDiv({ attr: { class: "SC-setting-group" } }))
            .setName("Execute button text")
            .addText(text => text
            .setValue(this.prompt.configuration.execute_button_text)
            .onChange(async (new_execute_button_text) => {
            this.prompt.configuration.execute_button_text = new_execute_button_text;
            await this.plugin.saveSettings();
        })
            .then((execute_button_text_component) => {
            // Autocomplete for the Execute button text.
            if (this.plugin.settings.show_autocomplete_menu) {
                createAutocomplete(this.plugin, execute_button_text_component.inputEl, () => execute_button_text_component.onChanged());
            }
        }));
        // Ok button
        const okButtonText = this.ok_button_text;
        if (okButtonText) {
            new obsidian.Setting(container_element)
                .addButton(button => button
                .setButtonText(okButtonText)
                .onClick(() => this.approve()));
        }
        // A tip about CSS styling.
        new obsidian.Setting(container_element)
            .setDesc("Tip! You can customise the style of the prompt modal with CSS by using the class ." + this.prompt.getCSSClass() + " or ." + Prompt.getCSSBaseClass() + " (for all prompt modals).");
    }
    approve() {
        if (this.on_after_approval) {
            this.approved = true;
            this.on_after_approval();
        }
        this.close();
    }
    onClose() {
        super.onClose();
        // Call a cancelling hook if one is defined (and if the closing happens due to cancelling, i.e. the ok button is NOT clicked).
        if (!this.approved && this.on_after_cancelling) {
            this.on_after_cancelling();
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class CustomShell extends Shell {
    constructor(plugin, customShellInstance) {
        super(plugin);
        this.customShellInstance = customShellInstance;
    }
    getBinaryPath() {
        return this.getConfiguration().binary_path;
    }
    getEscaper(rawValue) {
        const escaper = this.getConfiguration().escaper;
        switch (escaper) {
            case "PowerShell":
                return new PowerShellEscaper(rawValue);
            case "UnixShell":
                return new ShEscaper(rawValue);
            case null:
                // No escaping is wanted for this shell.
                return null;
            default:
                throw new Error("Unrecognised escaper: " + escaper);
        }
    }
    getIdentifier() {
        return this.customShellInstance.getId();
    }
    getName() {
        return this.getConfiguration().name;
    }
    getSupportedHostPlatforms() {
        return [this.getConfiguration().host_platform];
    }
    /**
     * @see CustomShellConfiguration.shell_platform
     * @private Can be made public if needed.
     */
    getShellPlatformId() {
        return this.getConfiguration().shell_platform ?? getOperatingSystem();
    }
    /**
     * @private Can be made public if needed.
     */
    shellPlatformIsWindows() {
        return this.getShellPlatformId() === "win32";
    }
    getPathSeparator() {
        return this.shellPlatformIsWindows() ? ";" : ":";
    }
    getDirectorySeparator() {
        return this.shellPlatformIsWindows() ? "\\" : "/";
    }
    translateAbsolutePath(originalPath) {
        // 1. Normalize the path before passing it to translator.
        const normalizedPath = normalizePath2(originalPath, this.getDirectorySeparator() === "\\"); // TODO: Think about changing normalizePath2() so that it would take the directory separator as a parameter, rather than a boolean. There's also a possible issue as normalizePath2() does not convert \ to / if needed, it can only convert the other way around, / to \ .
        // 2. Pass to a custom translator (if defined).
        return this.callAbsolutePathTranslator(normalizedPath);
    }
    translateRelativePath(originalPath) {
        // 1. Normalize the path before passing it to translator.
        const normalizedPath = normalizePath2(originalPath, this.getDirectorySeparator() === "\\"); // TODO: Same as above.
        // 2. Pass to a custom translator (if defined).
        // Actually, custom translator support for relative paths was removed before the custom shell feature was released,
        // as I can't think about any need for it. If needed, the feature can be added later, but it should use a separate
        // user defined function to make it cleaner - no need to have if (type === 'relative') { ... } code in the function.
        return normalizedPath;
    }
    callAbsolutePathTranslator(path) {
        const pathTranslatorCode = this.getConfiguration().path_translator;
        if (null === pathTranslatorCode) {
            // No translator is defined.
            // Return the path without modifications.
            return path;
        }
        try {
            // Create a JS function for doing the translation.
            const translatorFunction = new Function("absolutePath", // Parameter names
            pathTranslatorCode // Function content
            );
            const translatedPath = translatorFunction(path);
            if ("string" !== typeof translatedPath) {
                // noinspection ExceptionCaughtLocallyJS . It's ok to catch it locally, it will then be shown in an error balloon.
                throw new Error("Translator function returned " + (String(translatedPath) === "" ? typeof translatedPath : translatedPath) + " instead of a string.");
            }
            return translatedPath;
        }
        catch (error) {
            // Something failed.
            // Display an error balloon.
            this.plugin.newError(this.getName() + ": Translating path (" + path + ") failed: " + error.message);
            throw error; // Rethrow.
        }
    }
    async augmentSpawn(spawnAugmentation, tShellCommand, scEvent) {
        const debugLogBase = this.constructor.name + ".augmentSpawn(): ";
        const shellBinaryPath = this.getBinaryPath();
        if ("" === shellBinaryPath.trim()) {
            // Shell binary is not specified.
            this.plugin.newError("Custom shell " + this.getName() + ": Binary path is not defined.");
            return false; // Prevent execution.
        }
        // Disable Node.js's builtin shell feature.
        // CustomShells need to be able to define their own shell invoking command line options, so the Node.js's shell feature would be too limited.
        spawnAugmentation.spawnOptions.shell = false; // It's probably false by default in Node.js's spawn(), but make it explicit.
        // Windows specific settings.
        if (isWindows()) {
            const windowsSpecificSettings = this.getConfiguration().host_platform_configurations.win32;
            if (undefined === windowsSpecificSettings) {
                // Should not be undefined at this point. The settings file should contain a WindowsSpecificShellConfiguration is host_platform is "win32" (which it apparently is, if we got here).
                throw new Error("Windows specific CustomShell configuration is undefined.");
            }
            // If .quote_shell_arguments is false, deny Node.js's child_process.spawn() from:
            // - adding double quotes "" around arguments that contain spaces,
            // - post-fixing existing double quotes with backslashes \
            // Note that .quote_shell_arguments is inverted comparing to windowsVerbatimArguments.
            spawnAugmentation.spawnOptions.windowsVerbatimArguments = !windowsSpecificSettings.quote_shell_arguments;
        }
        // Define shell arguments.
        const shellCommandContentVariable = new Variable_ShellCommandContent(this.plugin, spawnAugmentation.shellCommandContent);
        const rawShellArguments = this.getConfiguration().shell_arguments;
        const parsedShellArguments = [];
        for (const rawShellArgument of rawShellArguments) {
            debugLog(debugLogBase + "Parsing shell argument: " + rawShellArgument);
            const shellArgumentParsingResult = await parseVariables(this.plugin, rawShellArgument, this, true, // Escape special characters, even though we are in a non-shell context atm. As arguments are parsed by a shell (after it's invoked), they will be used in the shell's context and might need escaping.
            tShellCommand, scEvent, mergeSets(this.plugin.getVariables(), new VariableSet([shellCommandContentVariable])));
            if (!shellArgumentParsingResult.succeeded) {
                // Shell argument parsing failed.
                debugLog(debugLogBase + "Parsing failed for shell argument: " + rawShellArgument);
                this.plugin.newErrors(shellArgumentParsingResult.error_messages);
                return false; // Deny execution.
            }
            debugLog(debugLogBase + "Shell argument " + rawShellArgument + " was successfully parsed to: " + shellArgumentParsingResult.parsed_content);
            parsedShellArguments.push(shellArgumentParsingResult.parsed_content);
        }
        spawnAugmentation.spawnArguments = parsedShellArguments;
        // Tell spawn() to use the shell binary path as an executable command.
        spawnAugmentation.shellCommandContent = shellBinaryPath; // Needs to come AFTER the original shellCommandContent is taken to spawnArguments!
        return true; // Allow execution.
    }
    _getShellCommandWrapper() {
        return this.getConfiguration().shell_command_wrapper;
    }
    getConfiguration() {
        return this.customShellInstance.configuration;
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
const UsageCategories = {
    // Keep in alphabetical order by 'singularName'! The order affects UI listings.
    customShells: {
        singularName: "Custom shell",
        pluralName: "Custom shells",
    },
    platforms: {
        singularName: "Operating system",
        pluralName: "Operating systems",
    },
    outputWrappers: {
        singularName: OutputWrapperModel.getSingularName(),
        pluralName: OutputWrapperModel.getPluralName(),
    },
    prompts: {
        singularName: PromptModel.getSingularName(),
        pluralName: PromptModel.getPluralName(),
    },
    promptFields: {
        singularName: PromptFieldModel.getSingularName(),
        pluralName: PromptFieldModel.getPluralName(),
    },
    shellCommands: {
        singularName: "Shell command",
        pluralName: "Shell commands",
    },
    variables: {
        singularName: "Variable",
        pluralName: "Variables",
    },
    // Keep in alphabetical order by 'singularName'!
};

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class UsageContainer {
    constructor(
    /**
     * Name of the entity whose usages are listed.
     */
    subjectName) {
        this.subjectName = subjectName;
        this.usagesByCategories = new Map();
    }
    addUsage(usage, usageCategoryId) {
        if (this.usagesByCategories.has(usageCategoryId)) {
            this.usagesByCategories.get(usageCategoryId)?.add(usage); // Use ?. because TypeScript does not understand that .get() in this situation never returns undefined.
        }
        else {
            this.usagesByCategories.set(usageCategoryId, new Set([usage]));
        }
    }
    addUsages(usages, usageCategory) {
        for (const usage of usages) {
            this.addUsage(usage, usageCategory);
        }
    }
    /**
     * Returns all usages in a 1-dimension Set.
     */
    getUsages() {
        const allUsages = new Set;
        this.usagesByCategories.forEach((usageSet) => usageSet.forEach((usage) => allUsages.add(usage)));
        return allUsages;
    }
    hasUsages() {
        let usageCategory;
        for (usageCategory of this.usagesByCategories.values()) {
            if (usageCategory.size > 0) {
                return true;
            }
        }
        return false;
    }
    countUsages() {
        let countUsages = 0;
        this.usagesByCategories.forEach((usageCategory) => countUsages += usageCategory.size);
        return countUsages;
    }
    /**
     * @private Can be made public, if needed.
     */
    countCategories() {
        return this.usagesByCategories.size;
    }
    /**
     * Returns a string containing 'title' properties from all usages. The usages are NOT grouped by category.
     */
    toSingleLineText() {
        return Array.from(this.getUsages()).map((usage) => usage.title).join(", ");
    }
    /**
     * Returns an HTMLDivElement containing a verbose listing of usages, grouped by categories.
     */
    toHTMLElement(prologueFormat) {
        const containerElement = document.createElement("div");
        // Preparations.
        const countUsages = this.countUsages();
        const countCategories = this.countCategories();
        const firstUsageCategoryId = (this.getUsageCategoryIds())[0];
        const isPlural = countUsages > 1;
        // Prologue text.
        let places;
        switch (countCategories) {
            case 1: {
                places = UsageCategories[firstUsageCategoryId][isPlural ? "pluralName" : "singularName"].toLocaleLowerCase();
                break;
            }
            default: {
                places = isPlural ? "places" : "place";
                break;
            }
        }
        let prologue;
        switch (prologueFormat) {
            case "long":
                if (countUsages === 0) {
                    prologue = `${this.subjectName} is used nowhere.`;
                }
                else {
                    prologue = `${this.subjectName} is used in ${countUsages} ${places}:`;
                }
                break;
            case "short":
                if (countUsages === 0) {
                    prologue = `Used nowhere.`;
                }
                else {
                    prologue = `Used in ${countUsages} ${places}:`;
                }
                break;
        }
        containerElement.createEl("p", { text: prologue });
        // List of usages.
        switch (countCategories) {
            case 1: {
                // Just one category.
                // List usages directly.
                this.createListElementForCategory(containerElement, firstUsageCategoryId);
                break;
            }
            default: {
                // Multiple categories.
                // Create nested usage lists.
                const mainListElement = containerElement.createEl("ul");
                for (const usageCategoryId of this.getUsageCategoryIds()) {
                    const usageCategoryListItemElement = mainListElement.createEl("li", { text: UsageCategories[usageCategoryId].pluralName + ":" });
                    this.createListElementForCategory(usageCategoryListItemElement, usageCategoryId); // Will create a nested <ul> element in an <li> element.
                }
                break;
            }
        }
        return containerElement;
    }
    /**
     * @private Can be made public, if needed.
     */
    getUsageCategoryIds() {
        return Array.from(this.usagesByCategories.keys());
    }
    createListElementForCategory(containerElement, usageCategoryId) {
        const listElement = containerElement.createEl("ul");
        for (const usage of this.usagesByCategories.get(usageCategoryId) ?? []) {
            listElement.createEl("li", { text: usage.title });
        }
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * ParsingProcess instances can be used in situations where it's uncertain can all variables be parsed at the time being,
 * i.e. when parsing shell commands (and aliases), as they can have preactions which require parsing to be done in two phases.
 *
 * Also, shell commands are often parsed in advance for command palette and context menus. Then it's good to store the parsing
 * result by using instances of this class.
 *
 * Then again, if the parsing use-case is simpler, e.g. Prompt description or prompt field values, it's more straightforward
 * to just call parseVariables() without utilising this class. After all, this class is a wrapper for parseVariables().
 *
 * <ParsingMap> is a generalization for defining keys for an object that will be used for submitting the original parseable
 * content. The same keys will then be used to form another object containing the parsing results.
 */
class ParsingProcess {
    constructor(plugin, original_contents, 
    /** Used to get a shell (getShell()) and default values for variables. */
    t_shell_command, sc_event, 
    /**
     * When .process() is called, it will shift and process the first VariableSet present in this array. So, the next call
     * will shift and process the next set.
     */
    variable_sets, 
    /**
     * This can be used to mark certain contents to always avoid escaping special characters in their variable values.
     * This should only be used for content that is never submitted to a shell, i.e. output wrappers at the moment.
     *
     * This is a list of 'content keys'.
     */
    avoid_escaping = []) {
        this.plugin = plugin;
        this.original_contents = original_contents;
        this.t_shell_command = t_shell_command;
        this.sc_event = sc_event;
        this.variable_sets = variable_sets;
        this.avoid_escaping = avoid_escaping;
        this.parsing_results = {};
        this.is_first_call = true;
        debugLog("Parsing process: Count variable sets: " + this.variable_sets.length);
    }
    /**
     * Performs the next step in the parsing process. The step can be the first one, or a subsequent step.
     *
     * @return True if parsing succeeded, false otherwise. Read the results by calling .getParsingResult().
     */
    async process() {
        if (this.variable_sets.length === 0) {
            throw new Error("No variable sets are left for processing.");
        }
        const current_variables = this.variable_sets.shift(); // as VariableSet: Tell TypeScript that there is always a set of variables.
        let success = true;
        debugLog("Parsing process: Count variables in current set: " + current_variables.size);
        // Multiple contents can be parsed in the same call. TShellCommand instances have 'shell_command' and 'alias'
        // contents which are parsed at the same time. This multi-content support can be used for even more situations if
        // needed in the future.
        for (const content_key of this.getContentKeys()) {
            let parse_content;
            if (this.is_first_call) {
                // Use original content.
                parse_content = this.original_contents[content_key];
                debugLog("Starting to parse '" + content_key + "': " + parse_content);
            }
            else {
                // Continue parsing content from previous parsing result. This time parse variables that were not parse back then.
                // FIXME: Problem: variable values that came from an earlier phase are exposed to repetitive parsing. Find a way to limit the parsing to only original parts of the shell command.
                const previousParsingResult = this.parsing_results[content_key];
                if (undefined === previousParsingResult) {
                    // This is just a type guard. this.getContentKeys() should only return keys that exist, so the checks should never throw errors in practise.
                    throw new Error("Parsing results do not contain key: " + content_key);
                }
                // Check that the previous parsing did not fail.
                if (null === previousParsingResult.parsed_content) {
                    // Previous parsing had probably failed.
                    throw new Error("Tried to continue parsing, but previous parsing result is null. Probably previous parsing has failed.");
                }
                // Previous parsing was ok.
                parse_content = previousParsingResult.parsed_content;
                debugLog("Continuing parsing '" + content_key + "': " + parse_content);
            }
            // Parse the variables
            const parsing_result = await parseVariables(this.plugin, parse_content, this.t_shell_command.getShell(), !this.avoidEscaping(content_key), this.t_shell_command, this.sc_event, current_variables);
            // Check if the parsing succeeded or failed.
            success = success && parsing_result.succeeded; // Flag as failed also if a previous phase has failed.
            // Store the parsing result
            this.mergeToParsingResults(content_key, parsing_result);
        }
        // Finish
        this.is_first_call = false;
        return success;
    }
    /**
     * A wrapper for .process() that processes all the VariableSets that are still left unprocessed.
     *
     * @return True if parsing all sets succeeded, false otherwise.
     */
    async processRest() {
        // 1. Check a previous parsing result (if exists).
        for (const content_key of this.getContentKeys()) {
            const parsingResult = this.parsing_results[content_key];
            if (parsingResult) {
                // A previous parsing result exists.
                // Ensure it has not failed.
                debugLog("Previous parsing succeeded? " + parsingResult.succeeded);
                if (!parsingResult.succeeded) {
                    // The previous parsing result has failed.
                    return false;
                }
            }
        }
        // 2. Process the rest of the VariableSets.
        for (let i = 0; i < this.variable_sets.length; i++) {
            if (!await this.process()) {
                return false;
            }
        }
        return true;
    }
    getParsingResults() {
        return this.parsing_results;
    }
    /**
     * Calls SC_Plugin.newErrors() to create visible error balloons for all the issues encountered during parsing.
     */
    displayErrorMessages() {
        this.plugin.newErrors(this.getErrorMessages());
    }
    getErrorMessages() {
        let error_messages = [];
        for (const content_key of this.getContentKeys()) {
            const parsingResult = this.parsing_results[content_key];
            // Type guard
            if (undefined === parsingResult) {
                // This should never happen, because this.getContentKeys() should only return existing keys.
                throw new Error("Parsing result is undefined.");
            }
            error_messages.push(...parsingResult.error_messages);
        }
        // Remove duplicate error messages. When parsing 'shell_command' and 'alias', they can contain same variables and
        // therefore generate same error messages.
        error_messages = uniqueArray(error_messages);
        return error_messages;
    }
    getContentKeys() {
        // TODO: This should not return names of properties whose value is undefined. Currently, parseVariables() is called with undefined parseable content. It's been like that for a long time (as long as output wrappers have existed, as they are the first optional parseable content) and has just by chance not broken anything.
        return Object.getOwnPropertyNames(this.original_contents);
    }
    /**
     * Merges consecutive parsing results together so that information from both the old and new parsing results can be preserved.
     */
    mergeToParsingResults(content_key, newParsingResult) {
        const originalParsingResult = this.parsing_results[content_key];
        if (undefined === originalParsingResult) {
            // No need to merge. But clone the object so that possible future merges will not mess up the original object
            // in case it's used somewhere else.
            this.parsing_results[content_key] = cloneObject(newParsingResult);
            // Note that originalParsingResult is still undefined. If you continue writing code here or after the if block
            // (near the end of the function), do something like originalParsingResult = this.parsing_results[content_key]
            // but that would require changing originalParsingResult from const to let.
        }
        else {
            // Merge
            // NOTE: originalParsingResult.original_content IS KEPT UNCHANGED! The newer "original" content is not actually original, because it's partly parsed. That's why the old one is preserved.
            originalParsingResult.parsed_content = newParsingResult.parsed_content; // New parsed content overrides the old one.
            originalParsingResult.succeeded &&= newParsingResult.succeeded; // Both the old and new parsing must have succeeded in order to consider the whole process succeeded.
            originalParsingResult.error_messages.push(...newParsingResult.error_messages); // Include both old and new error messages.
            originalParsingResult.count_parsed_variables += newParsingResult.count_parsed_variables; // Sum up the variable usage counts. At the time of writing, the sum is only used for determining if there were any variables parsed or not, so an accurate sum is not used atm.
        }
    }
    /**
     * Tells whether the given content_key has a mark that special characters in the content's variable values should not be escaped.
     *
     * @param content_key
     * @private
     */
    avoidEscaping(content_key) {
        return this.avoid_escaping.contains(content_key);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function newShellCommandConfiguration(shell_command_id, shell_command = "") {
    return {
        id: shell_command_id,
        platform_specific_commands: {
            default: shell_command,
        },
        shells: {},
        alias: "",
        icon: null,
        confirm_execution: false,
        ignore_error_codes: [],
        input_contents: {
            stdin: null,
        },
        output_handlers: {
            stdout: OutputChannel.getDefaultConfiguration("ignore"),
            stderr: OutputChannel.getDefaultConfiguration("notification"),
        },
        output_wrappers: {
            stdout: null,
            stderr: null,
        },
        output_channel_order: "stdout-first",
        output_handling_mode: "buffered",
        execution_notification_mode: null,
        events: {},
        debounce: null,
        command_palette_availability: "enabled",
        preactions: [],
        variable_default_values: {},
    };
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
async function RunMigrations(plugin) {
    const should_save = [
        EnsureMainFieldsExist(plugin),
        MigrateCommandsToShellCommands(plugin),
        MigrateShellCommandsObjectToArray(plugin),
        MigrateShellCommandToPlatforms(plugin),
        MigrateShellCommandOutputChannels(plugin),
        EnsureShellCommandsHaveAllFields(plugin),
        EnsureCustomVariablesHaveAllFields(plugin),
        EnsurePromptFieldsHaveAllFields(plugin),
        DeleteEmptyCommandsField(plugin),
        MigrateDebouncingModes(plugin),
        MigrateOldIconNames(plugin),
    ];
    if (should_save.includes(true)) {
        // Only save if there were changes to configuration.
        debugLog("Saving migrations...");
        backupSettingsFile(plugin); // Make a backup copy of the old file BEFORE writing the new, migrated settings file.
        await plugin.saveSettings();
        debugLog("Migrations saved...");
    }
}
/**
 * Temporary, only needed for migrating a debouncing `mode` property which was present in SC 0.22.0-beta.1, so not in any
 * actual release. The property is removed in 0.22.0-beta.2. Keep this function a few months, then it's not needed anymore.
 * @param plugin
 * @constructor
 */
function MigrateDebouncingModes(plugin) {
    let save = false;
    for (const shellCommandConfiguration of plugin.settings.shell_commands) {
        if (shellCommandConfiguration.debounce) {
            // @ts-ignore
            if (undefined !== shellCommandConfiguration.debounce.mode) {
                // Found a `mode` property that was present in SC 0.22.0-beta.1 .
                // @ts-ignore
                switch (shellCommandConfiguration.debounce.mode) {
                    case "early-and-late-execution":
                        shellCommandConfiguration.debounce.executeEarly = true;
                        shellCommandConfiguration.debounce.executeLate = true;
                        break;
                    case "early-execution":
                        shellCommandConfiguration.debounce.executeEarly = true;
                        shellCommandConfiguration.debounce.executeLate = false;
                        break;
                    case "late-execution":
                        shellCommandConfiguration.debounce.executeEarly = false;
                        shellCommandConfiguration.debounce.executeLate = true;
                        break;
                }
                // @ts-ignore
                debugLog("Migration: Shell command #" + shellCommandConfiguration.id + " had a deprecated debounce.mode property (" + shellCommandConfiguration.debounce.mode + "). It was migrated to debounce.executeEarly (" + (shellCommandConfiguration.debounce.executeEarly ? "true" : "false") + ") and debounce.executeLate (" + (shellCommandConfiguration.debounce.executeLate ? "true" : "false") + ").");
                // @ts-ignore
                delete shellCommandConfiguration.debounce.mode;
                save = true;
            }
            // @ts-ignore
            if (undefined !== shellCommandConfiguration.debounce.cooldown) {
                // `cooldown` was present in 0.22.0-beta.1, but renamed in 0.22.0-beta.2.
                // @ts-ignore
                shellCommandConfiguration.debounce.cooldownDuration = shellCommandConfiguration.debounce.cooldown;
                // @ts-ignore
                delete shellCommandConfiguration.debounce.cooldown;
                save = true;
            }
            // prolongCooldown was not present in 0.22.0-beta.1. Add it, but disable it by default.
            if (undefined === shellCommandConfiguration.debounce.prolongCooldown) {
                shellCommandConfiguration.debounce.prolongCooldown = false;
                save = true;
            }
        }
    }
    return save;
}
/**
 * Can be removed in the future, but I haven't yet decided will it be done in 1.0 or later.
 */
function MigrateShellCommandsObjectToArray(plugin) {
    // Check if the shell commands' container is an object.
    if (!Array.isArray(plugin.settings.shell_commands)) {
        // It is an object. It needs to be changed to an array in order to allow custom ordering.
        const shell_commands_array = [];
        for (const shell_command_id of Object.getOwnPropertyNames(plugin.settings.shell_commands)) { // Remember that plugin.settings.shell_commands is an object here! Not an array (yet).
            // @ts-ignore I don't know why TypeScript thinks the index is incorrect.
            const shell_command_configuration = plugin.settings.shell_commands[shell_command_id];
            // Assign 'id' to ShellCommandConfiguration because it did not contain it before this migration.
            shell_command_configuration.id = shell_command_id;
            // Add the ShellCommandConfiguration to the new array container.
            shell_commands_array.push(shell_command_configuration);
        }
        // Replace the old object container with the new array container.
        plugin.settings.shell_commands = shell_commands_array; // Now plugin.settings.shell_commands changes to be an array instead of an object.
        return true; // Save the changes.
    }
    else {
        // The container is already migrated.
        return false; // No need to save anything.
    }
}
/**
 * Can be removed in 1.0.0.
 *
 * @param plugin
 * @constructor
 */
function MigrateCommandsToShellCommands(plugin) {
    if (undefined === plugin.settings.commands) {
        return false;
    }
    const count_shell_commands = plugin.settings.commands.length;
    let save = false;
    if (0 < count_shell_commands) {
        let count_empty_commands = 0; // A counter for empty or null commands
        debugLog("settings.commands is not empty, will migrate " + count_shell_commands + " commands to settings.shell_commands.");
        for (const shell_command_id in plugin.settings.commands) {
            const shell_command = plugin.settings.commands[shell_command_id];
            // Ensure that the command is not empty. Just in case.
            if (null === shell_command || 0 === shell_command.length) {
                // The command is empty
                debugLog("Migration failure for shell command #" + shell_command_id + ": The original shell command string is empty, so it cannot be migrated.");
                count_empty_commands++;
            }
            else if (undefined !== plugin.getShellCommandConfigurationIndex(shell_command_id)) {
                // A command with the same id already exists
                debugLog("Migration failure for shell command #" + shell_command_id + ": A shell command with same ID already exists in settings.shell_commands.");
            }
            else {
                // All OK, migrate.
                plugin.settings.shell_commands.push(newShellCommandConfiguration(shell_command_id, shell_command)); // Creates a shell command with default values and defines the command for it.
                delete plugin.settings.commands[shell_command_id]; // Leaves a null in place, but we can deal with it by deleting the whole array if it gets empty.
                count_empty_commands++; // Account the null generated on the previous line.
                save = true;
                debugLog("Migrated shell command #" + shell_command_id + ": " + shell_command);
            }
        }
        if (count_empty_commands === count_shell_commands) {
            // The whole commands array now contains only empty/null commands.
            // Delete it.
            delete plugin.settings.commands;
        }
    }
    else {
        debugLog("settings.commands is empty, so no need to migrate commands. Good thing! :)");
    }
    return save;
}
function MigrateShellCommandOutputChannels(plugin) {
    let save = false;
    const shellCommandConfigurations = plugin.settings.shell_commands;
    for (const shellCommandConfiguration of shellCommandConfigurations) {
        let outputStream;
        // Iterate "stdout" and "stderr".
        // @ts-ignore
        if (shellCommandConfiguration.output_channels) {
            for (outputStream in shellCommandConfiguration.output_channels) {
                const outputChannel = shellCommandConfiguration.output_channels[outputStream];
                debugLog("Shell command #" + shellCommandConfiguration.id + ": Migrating output stream " + outputStream + " to use a configuration object.");
                if (!shellCommandConfiguration.output_handlers) {
                    // @ts-ignore Don't yell about the empty object, it will soon have content.
                    shellCommandConfiguration.output_handlers = {};
                }
                shellCommandConfiguration.output_handlers[outputStream] = OutputChannel.getDefaultConfiguration(outputChannel);
            }
            delete shellCommandConfiguration.output_channels;
            save = true;
        }
    }
    return save;
}
/**
 * This is a general migrator that adds new, missing properties to ShellCommandConfiguration objects. This is not tied to any specific version update, unlike MigrateCommandsToShellCommands().
 *
 * @param plugin
 * @constructor
 */
function EnsureShellCommandsHaveAllFields(plugin) {
    let save = false;
    const shell_command_default_configuration = newShellCommandConfiguration("no-id"); // Use a dummy id here, because something needs to be used. This id should never end up being used in practice.
    const shell_command_configurations = plugin.settings.shell_commands;
    for (const shell_command_configuration of shell_command_configurations) {
        for (const property_name in shell_command_default_configuration) {
            // @ts-ignore property_default_value can have (almost) whatever datatype
            const property_default_value = shell_command_default_configuration[property_name];
            // @ts-ignore
            if (undefined === shell_command_configuration[property_name] && property_name !== "id") { // The "id" check is just in case that MigrateShellCommandsObjectToArray() would not have added the "id" property, in which case the dummy "no-id" id should not be accidentally assigned to the shell command.
                // This shell command does not have this property.
                // Add the property to the shell command and use a default value.
                debugLog("EnsureShellCommandsHaveAllFields(): Shell command #" + shell_command_configuration.id + " does not have a property '" + property_name + "'. Will create the property and assign a default value '" + property_default_value + "'.");
                // @ts-ignore
                shell_command_configuration[property_name] = property_default_value;
                save = true;
            }
        }
    }
    return save;
}
function EnsureCustomVariablesHaveAllFields(plugin) {
    let save = false;
    const customVariableModel = getModel(CustomVariableModel.name);
    const customVariableDefaultConfiguration = customVariableModel.getDefaultConfiguration();
    let customVariableConfiguration;
    for (customVariableConfiguration of plugin.settings.custom_variables) {
        for (const propertyName in customVariableDefaultConfiguration) {
            // @ts-ignore propertyDefaultValue can have (almost) whatever datatype
            const propertyDefaultValue = customVariableDefaultConfiguration[propertyName];
            // @ts-ignore
            if (undefined === customVariableConfiguration[propertyName]) {
                // This custom variable does not have this property.
                // Add the property to it and use a default value.
                debugLog("EnsureCustomVariablesHaveAllFields(): Custom variable #" + customVariableConfiguration.id + " does not have a property '" + propertyName + "'. Will create the property and assign a default value '" + propertyDefaultValue + "'.");
                // @ts-ignore
                customVariableConfiguration[propertyName] = propertyDefaultValue;
                save = true;
            }
        }
    }
    return save;
}
function EnsurePromptFieldsHaveAllFields(plugin) {
    let save = false;
    const promptFieldModel = getModel(PromptFieldModel.name);
    let promptConfiguration;
    for (promptConfiguration of plugin.settings.prompts) {
        let promptFieldConfiguration;
        for (promptFieldConfiguration of promptConfiguration.fields) {
            const defaultPromptFieldConfiguration = promptFieldModel.getDefaultConfiguration(promptFieldConfiguration.type ?? "single-line-text" // SC versions < 0.21.0 did not define 'type' property for prompt field configurations.
            );
            for (const propertyName in defaultPromptFieldConfiguration) {
                // @ts-ignore propertyDefaultValue can have (almost) whatever datatype
                const propertyDefaultValue = defaultPromptFieldConfiguration[propertyName];
                // @ts-ignore
                if (undefined === promptFieldConfiguration[propertyName]) {
                    // This PromptField does not have this property.
                    debugLog("EnsurePromptFieldsHaveAllFields(): PromptField '" + promptFieldConfiguration.label + "' does not have a property '" + propertyName + "'. Will create the property and assign a default value '" + propertyDefaultValue + "'.");
                    // @ts-ignore
                    promptFieldConfiguration[propertyName] = propertyDefaultValue;
                    save = true;
                }
            }
        }
    }
    return save;
}
/**
 * This is a general migrator that adds new, missing properties to the main settings object. This is not tied to any specific version update, unlike MigrateCommandsToShellCommands().
 *
 * @param plugin
 * @constructor
 */
function EnsureMainFieldsExist(plugin) {
    let has_missing_fields = false;
    const settings = plugin.settings;
    const default_settings = getDefaultSettings(false);
    for (const property_name in default_settings) {
        // @ts-ignore
        if (undefined === settings[property_name]) {
            // The settings object does not have this property.
            // @ts-ignore property_default_value can have (almost) whatever datatype
            const property_default_value = default_settings[property_name];
            debugLog("EnsureMainFieldsExist(): Main settings does not have property '" + property_name + "'. Will later create the property and assign a default value '" + property_default_value + "'.");
            has_missing_fields = true;
        }
    }
    if (has_missing_fields) {
        debugLog("EnsureMainFieldsExist(): Doing the above-mentioned new field creations...");
        plugin.settings = combineObjects(default_settings, plugin.settings);
        debugLog("EnsureMainFieldsExist(): Done.");
        return true; // Save the changes
    }
    debugLog("EnsureMainFieldsExist(): No new fields to create, all ok.");
    return false; // Nothing to save.
}
/**
 * Can be removed in 1.0.0.
 *
 * @param plugin
 * @constructor
 */
function MigrateShellCommandToPlatforms(plugin) {
    let save = false;
    for (const shell_command_configuration of plugin.settings.shell_commands) {
        if (undefined !== shell_command_configuration.shell_command) {
            // The shell command should be migrated.
            if (undefined === shell_command_configuration.platform_specific_commands || shell_command_configuration.platform_specific_commands.default === "") {
                debugLog("Migrating shell command #" + shell_command_configuration.id + ": shell_command string will be moved to platforms.default: " + shell_command_configuration.shell_command);
                shell_command_configuration.platform_specific_commands = {
                    default: shell_command_configuration.shell_command,
                };
                delete shell_command_configuration.shell_command;
                save = true;
            }
            else {
                debugLog("Migration failure for shell command #" + shell_command_configuration.id + ": platforms exists already.");
            }
        }
    }
    return save;
}
/**
 * Can be removed in 1.0.0.
 *
 * @param plugin
 * @constructor
 */
function DeleteEmptyCommandsField(plugin) {
    let save = false;
    if (undefined !== plugin.settings.commands) {
        if (plugin.settings.commands.length === 0) {
            delete plugin.settings.commands;
            save = true;
        }
    }
    return save;
}
function MigrateOldIconNames(plugin) {
    let save = false;
    for (const shellCommandConfiguration of plugin.settings.shell_commands) {
        if (null !== shellCommandConfiguration.icon) {
            if (ICON_MIGRATIONS.hasOwnProperty(shellCommandConfiguration.icon)) {
                // This icon needs to be migrated. (It's still the same icon image).
                const newIconIdOrNull = ICON_MIGRATIONS[shellCommandConfiguration.icon]; // If this is null, then it's one of five iconIds that existed in the icon list of SC <= 0.22.0, but that didn't actually work (= showed no icon). Just disable the icon.
                if (null === newIconIdOrNull) {
                    debugLog("Migrated shell command #" + shellCommandConfiguration.id + " icon: Old icon name " + shellCommandConfiguration.icon + " was faulty and never showed an icon image, so the icon is now removed.");
                }
                else {
                    debugLog("Migrated shell command #" + shellCommandConfiguration.id + " icon: Old icon name: " + shellCommandConfiguration.icon + ". New icon name: " + newIconIdOrNull);
                }
                shellCommandConfiguration.icon = newIconIdOrNull;
                save = true;
            }
        }
    }
    return save;
}
/**
 * Permanent, do not remove.
 *
 * @param plugin
 */
function backupSettingsFile(plugin) {
    // plugin.app.fileManager.
    // @ts-ignore
    const current_settings_version = (plugin.settings.settings_version === "prior-to-0.7.0") ? "0.x" : plugin.settings.settings_version;
    const plugin_path = getPluginAbsolutePath(plugin, isWindows());
    const settings_file_path = path__namespace.join(plugin_path, "data.json");
    const backup_file_path_without_extension = path__namespace.join(plugin_path, "data-backup-version-" + current_settings_version + "-before-upgrading-to-" + SC_Plugin.SettingsVersion);
    // Check that the current settings file can be found.
    if (!fs__namespace.existsSync(settings_file_path)) {
        // Not found. Probably the vault uses a different config folder than .obsidian.
        debugLog("backupSettingsFile(): Cannot find data.json");
        plugin.newError("Shell commands: Cannot create a backup of current settings file, because data.json is not found.");
        return;
    }
    let backup_file_path = backup_file_path_without_extension + ".json";
    let running_number = 1;
    while (fs__namespace.existsSync(backup_file_path)) {
        running_number++; // The first number will be 2.
        backup_file_path = backup_file_path_without_extension + "-" + running_number + ".json";
        if (running_number >= 1000) {
            // There is some problem with detecting existing/inexisting files.
            // Prevent hanging the program in an eternal loop.
            throw new Error("backupSettingsFile(): Eternal loop detected.");
        }
    }
    fs__namespace.copyFileSync(settings_file_path, backup_file_path);
}
// TODO: Add migration: shell command variable_default_values: if type is "show-errors", change it to "inherit", but only if old settings version was below 18.

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function createShellSelectionFields(plugin, containerElement, shells, is_global_settings, onChange) {
    const shellSelectionSettings = {
        darwin: new obsidian.Setting(containerElement),
        linux: new obsidian.Setting(containerElement),
        win32: new obsidian.Setting(containerElement),
    };
    let platform_id;
    for (platform_id in PlatformNames) {
        const platform_name = PlatformNames[platform_id];
        let options;
        if (is_global_settings) {
            const currentSystemDefault = (getOperatingSystem() === platform_id) ? " (" + extractFileName(getUsersDefaultShellIdentifier()) + ")" : "";
            options = { "default": "Use system default" + currentSystemDefault };
        }
        else {
            const defaultShell = plugin.getDefaultShellForPlatform(platform_id);
            options = {
                "default": "Use default (" + (defaultShell?.getName() ?? "system default") + ")",
            };
        }
        // Get human-readable shell names
        for (const shell of getShellsForPlatform(platform_id)) {
            options[shell.getIdentifier()] = shell.getName();
        }
        const setting = shellSelectionSettings[platform_id];
        setting
            .setName(platform_name + (is_global_settings ? " default shell" : " shell"))
            .setDesc((is_global_settings ? "Can be overridden by each shell command. " : "") + ("win32" === platform_id ? "Powershell is recommended over cmd.exe, because this plugin does not support escaping variables in CMD." : ""))
            .addDropdown(dropdown => dropdown
            .addOptions(options)
            .setValue(shells[platform_id] ?? "default")
            .onChange(((_platform_id) => {
            return async (value) => {
                if ("default" === value) {
                    // When using default shell, the value should be unset.
                    delete shells[_platform_id];
                }
                else {
                    // Normal case: assign the shell value.
                    shells[_platform_id] = value;
                }
                await plugin.saveSettings();
                onChange?.(_platform_id);
            };
        })(platform_id)));
    }
    return shellSelectionSettings;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function createPlatformSpecificShellCommandField(plugin, container_element, t_shell_command, platform_id, show_autocomplete_menu, onChange) {
    const platform_name = getPlatformName(platform_id);
    const setting_group = CreateShellCommandFieldCore(plugin, container_element, "Shell command on " + platform_name, t_shell_command.getPlatformSpecificShellCommands()[platform_id] ?? "", t_shell_command.getShellForPlatform(platform_id), t_shell_command, show_autocomplete_menu, async (shell_command) => {
        if (shell_command.length) {
            // shell_command is not empty, so it's a normal command.
            t_shell_command.getPlatformSpecificShellCommands()[platform_id] = shell_command;
        }
        else {
            // shell_command is empty, so the default command should be used.
            delete t_shell_command.getPlatformSpecificShellCommands()[platform_id];
        }
        await plugin.saveSettings();
        onChange?.();
    }, undefined, // No need to know when shell command preview is rendered, at least at the moment.
    t_shell_command.getDefaultShellCommand());
    setting_group.name_setting.setDesc("If empty, the default shell command will be used on " + platform_name + ".");
    return setting_group;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
function createTabs(container_element, tabs, activateTabId) {
    const tab_header = container_element.createEl("div", { attr: { class: "SC-tab-header" } });
    const tab_content_containers = {};
    const tab_buttons = {};
    const tab_structure = {
        header: tab_header,
        active_tab_id: Object.keys(tabs)[0],
        buttons: tab_buttons,
        contentContainers: tab_content_containers,
        contentGeneratorPromises: {},
    };
    for (const tab_id in tabs) {
        const tab = tabs[tab_id];
        // Create button
        const button = tab_header.createEl("button", {
            attr: {
                class: "SC-tab-header-button",
                activateTab: "SC-tab-" + tab_id,
            },
        });
        button.onclick = function (event) {
            const tab_button = this; // Use 'this' instead of event.target because this way we'll always get a button element, not an element inside the  button (i.e. an icon).
            // Hide all tab contents and get the max dimensions
            let max_width = 0;
            let max_height = 0;
            const tab_header = tab_button.parentElement;
            if (null === tab_header) {
                throw new Error("Tab header is missing. Did not get a parent from tab button.");
            }
            const container_element = tab_header.parentElement;
            if (null === container_element) {
                throw new Error("Container element is missing. Did not get a parent from tab header.");
            }
            const tab_contents = container_element.findAll("div.SC-tab-content"); // Do not get all tab contents that exist, because there might be multiple tab systems open at the same time.
            const is_main_settings_modal = container_element.hasClass("vertical-tab-content");
            for (const index in tab_contents) {
                const tab_content = tab_contents[index];
                // Get the maximum tab dimensions so that all tabs can have the same dimensions.
                // But don't do it if this is the main settings modal
                if (!is_main_settings_modal) {
                    tab_content.addClass("SC-tab-active"); // Need to make the tab visible temporarily in order to get the dimensions.
                    if (tab_content.offsetHeight > max_height) {
                        max_height = tab_content.offsetHeight;
                    }
                    if (tab_content.offsetWidth > max_width) {
                        max_width = tab_content.offsetWidth;
                    }
                }
                // Finally hide the tab
                tab_content.removeClass("SC-tab-active");
            }
            // Remove active status from all buttons
            const adjacent_tab_buttons = tab_header.findAll(".SC-tab-header-button"); // Do not get all tab buttons that exist, because there might be multiple tab systems open at the same time.
            for (const index in adjacent_tab_buttons) {
                const tab_button = adjacent_tab_buttons[index];
                tab_button.removeClass("SC-tab-active");
            }
            // Activate the clicked tab
            tab_button.addClass("SC-tab-active");
            const activateTabAttribute = tab_button.attributes.getNamedItem("activateTab");
            if (null === activateTabAttribute) {
                throw new Error("Tab button has no 'activateTab' HTML attribute! Murr!");
            }
            const activate_tab_id = activateTabAttribute.value;
            const tab_content = document.getElementById(activate_tab_id);
            if (null === tab_content) {
                throw new Error("No tab content was found with activate_tab_id '" + activate_tab_id + "'! Hmph!");
            }
            tab_content.addClass("SC-tab-active");
            // Mark the clicked tab as active in TabStructure (just to report which tab is currently active)
            tab_structure.active_tab_id = activate_tab_id.replace(/^SC-tab-/, ""); // Remove "SC-tab" prefix.
            // Focus an element (if a focusable element is present)
            tab_content.find(".SC-focus-element-on-tab-opening")?.focus(); // ? = If not found, do nothing.
            // Apply the max dimensions to this tab
            // But don't do it if this is the main settings modal
            if (!is_main_settings_modal) {
                tab_content.style.width = max_width + "px";
                tab_content.style.height = max_height + "px";
            }
            // Do nothing else (I don't know if this is needed or not)
            event.preventDefault();
        };
        obsidian.setIcon(button, tab.icon);
        button.insertAdjacentText("beforeend", " " + tab.title);
        tab_buttons[tab_id] = button;
        // Create content container
        tab_content_containers[tab_id] = container_element.createEl("div", { attr: { class: "SC-tab-content", id: "SC-tab-" + tab_id } });
        // Generate content
        tab_structure.contentGeneratorPromises[tab_id] = tab.content_generator(tab_content_containers[tab_id]);
    }
    // Open a tab.
    tab_buttons[activateTabId].click();
    // Return the TabStructure
    return tab_structure;
}

function createExecutionNotificationField(containerElement, currentValue, defaultValue, notificationMessageDuration, onChange) {
    const executionNotificationOptions = {
        "default": "",
        "disabled": "Do not show",
        "quick": "Show for " + notificationMessageDuration + " seconds",
        "permanent": "Show until the process is finished",
        "if-long": "Show only if executing takes long",
    };
    let title;
    if (false === defaultValue) {
        // Main settings.
        title = "Show a notification when executing shell commands";
        delete executionNotificationOptions.default;
    }
    else {
        // Shell command specific settings.
        title = "Show a notification when executing";
        executionNotificationOptions["default"] = "Default (" + executionNotificationOptions[defaultValue] + ")";
    }
    new obsidian.Setting(containerElement)
        .setName(title)
        .addDropdown(dropdown_component => dropdown_component
        .addOptions(executionNotificationOptions)
        .setValue((currentValue === null) ? "default" : currentValue)
        .onChange((newExecutionNotificationMode) => {
        if ("default" === newExecutionNotificationMode && defaultValue !== false) {
            // Change 'default' to null.
            // @ts-ignore Null is allowed in the onChange() function's signature.
            onChange(null);
        }
        else {
            // newExecutionNotificationMode is a real ExecutionNotificationMode value.
            onChange(newExecutionNotificationMode);
        }
    }));
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class ShellCommandSettingsModal extends SC_Modal {
    constructor(plugin, shell_command_id, setting_tab) {
        super(plugin);
        this.shell_command_id = shell_command_id;
        this.t_shell_command = plugin.getTShellCommands()[shell_command_id];
        this.name_setting = setting_tab.setting_groups[shell_command_id].name_setting;
        this.settingGroupInMainTab = setting_tab.setting_groups[shell_command_id];
        this.setting_tab = setting_tab;
    }
    onOpen() {
        super.onOpen();
        // Modal title.
        this.setTitle(this.t_shell_command.getAliasOrShellCommand());
        // Tabs
        this.tab_structure = createTabs(this.modalEl, {
            "extra-options-general": {
                title: "General",
                icon: "gear",
                content_generator: (container_element) => this.tabGeneral(container_element),
            },
            "extra-options-preactions": {
                title: "Preactions",
                icon: "note-glyph",
                content_generator: (container_element) => this.tabPreactions(container_element),
            },
            "extra-options-output": {
                title: "Output",
                icon: "lines-of-text",
                content_generator: (container_element) => this.tabOutput(container_element),
            },
            "extra-options-environments": {
                title: "Environments",
                icon: "stacked-levels",
                content_generator: (container_element) => this.tabEnvironments(container_element),
            },
            "extra-options-events": {
                title: "Events",
                icon: "dice",
                content_generator: (container_element) => this.tabEvents(container_element),
            },
            "extra-options-variables": {
                title: "Variables",
                icon: "code-glyph",
                content_generator: (container_element) => this.tabVariables(container_element),
            },
        }, "extra-options-general");
        // Hotkeys for moving to next/previous shell command
        const switch_to_t_shell_command = (t_shell_command) => {
            const new_modal = new ShellCommandSettingsModal(this.plugin, t_shell_command.getId(), this.setting_tab);
            this.close(); // Needs to be closed before the new one is opened, otherwise the new one's tab content won't be shown.
            new_modal.open();
            new_modal.activateTab(this.tab_structure.active_tab_id);
        };
        this.scope.register(["Mod"], "ArrowUp", () => {
            const previousTShellCommand = this.t_shell_command.previousTShellCommand();
            if (previousTShellCommand) {
                switch_to_t_shell_command(previousTShellCommand);
            }
        });
        this.scope.register(["Mod"], "ArrowDown", () => {
            const nextTShellCommand = this.t_shell_command.nextTShellCommand();
            if (nextTShellCommand) {
                switch_to_t_shell_command(nextTShellCommand);
            }
        });
        const bottomSetting = new obsidian.Setting(this.modalEl)
            .setDesc("Tip! Hit " + CmdOrCtrl() + " + up/down to switch to previous/next shell command.");
        createExecuteNowButton(this.plugin, bottomSetting, this.t_shell_command);
    }
    async tabGeneral(container_element) {
        // Alias field
        const alias_container = container_element.createDiv({ attr: { class: "SC-setting-group" } });
        new obsidian.Setting(alias_container)
            .setName("Alias");
        const on_alias_change = async (value) => {
            // Change the actual alias value
            this.t_shell_command.getConfiguration().alias = value;
            // Update Obsidian command palette
            this.t_shell_command.renameObsidianCommand(this.t_shell_command.getAliasOrShellCommand());
            // UpdateShell commands settings panel
            this.name_setting.nameEl.innerHTML = generateShellCommandFieldIconAndName(this.t_shell_command);
            // Update this modal's title.
            this.setTitle(this.t_shell_command.getAliasOrShellCommand());
            // Save
            await this.plugin.saveSettings();
        };
        const alias_setting = new obsidian.Setting(alias_container)
            .addText(text => text
            .setValue(this.t_shell_command.getAlias())
            .onChange(on_alias_change))
            .setClass("SC-no-description");
        const alias_input_element = alias_setting.controlEl.find("input");
        alias_input_element.addClass("SC-focus-element-on-tab-opening"); // Focus without a need to click the field.
        if (this.plugin.settings.show_autocomplete_menu) {
            // Show autocomplete menu (= a list of available variables).
            createAutocomplete(this.plugin, alias_input_element, on_alias_change);
        }
        alias_container.createEl("p", { text: "If not empty, the alias will be displayed in the command palette instead of the actual command. An alias is never executed as a command." });
        alias_container.createEl("p", { text: "You can also use the same {{}} style variables in aliases that are used in shell commands. When variables are used in aliases, they do not affect the command execution in any way, but it's a nice way to reveal what values your command will use, even when an alias hides most of the other technical details. Starting a variable with {{! will prevent escaping special characters in command palette." });
        // Icon field
        const current_icon = this.t_shell_command.getConfiguration().icon;
        const icon_setting = new obsidian.Setting(container_element)
            .setDesc("If defined, the icon will be shown in file menu, folder menu, and editor menu in front of the alias text. It's also shown in the settings. It makes it easier to distinguish different shell commands visually from each other.")
            .addDropdown(dropdown => dropdown
            .addOption("no-icon", "No icon") // Need to use a non-empty string like "no-icon", because if 'value' would be "" then it becomes the same as 'display' from some reason, i.e. "No icon".
            .then((dropdown) => {
            // Iterate all available icons.
            for (const icon of SORTED_ICON_LIST) {
                // Create an option for the icon.
                dropdown.addOption(icon.iconId, icon.displayName);
            }
            dropdown.setValue(current_icon ?? "no-icon"); // "" == the 'No icon' option.
        })
            .onChange(async (new_icon) => {
            if ("no-icon" === new_icon) {
                // Disable icon
                this.t_shell_command.getConfiguration().icon = null;
                // Remove the icon from the modal
                icon_setting.nameEl.innerHTML = "Icon";
            }
            else {
                // Set or change the icon
                this.t_shell_command.getConfiguration().icon = new_icon;
                // Update the icon in the modal
                icon_setting.nameEl.innerHTML = "Icon " + getIconHTML(new_icon);
            }
            // Update (or remove) the icon in the main settings panel
            this.name_setting.nameEl.innerHTML = generateShellCommandFieldIconAndName(this.t_shell_command);
            // Save settings
            await this.plugin.saveSettings();
        }));
        icon_setting.nameEl.innerHTML = "Icon " + (current_icon ? getIconHTML(current_icon) : "");
        // Confirm execution field
        new obsidian.Setting(container_element)
            .setName("Ask confirmation before execution")
            .addToggle(toggle => toggle
            .setValue(this.t_shell_command.getConfirmExecution())
            .onChange(async (value) => {
            this.t_shell_command.getConfiguration().confirm_execution = value;
            const icon_container = this.name_setting.nameEl.find("span.shell-commands-confirm-execution-icon-container");
            if (this.t_shell_command.getConfirmExecution()) {
                // Show icon
                icon_container.removeClass("SC-hide");
            }
            else {
                // Hide icon
                icon_container.addClass("SC-hide");
            }
            await this.plugin.saveSettings();
        }));
        // Stdin field
        new obsidian.Setting(container_element)
            .setName("Pass variables to standard input (stdin) (experimental)")
            .setDesc("Used to pass long texts as input to the shell command. There is a limit to command line length, and e.g. {{note_content}} might provide a value too long to be used as an argument, so it works better when passed to stdin. Also, programs that ask multiple values interactively, can be fed with values using stdin. If there are multiple values that need to be inputted, put them on separate lines. Many shell programs interpret newlines as separators between different values.")
            .addExtraButton(extraButtonComponent => extraButtonComponent
            .setIcon("help")
            .setTooltip("Documentation: Pass variables to stdin")
            .onClick(() => gotoURL(Documentation.variables.passVariablesToStdin)));
        const stdinSettingContainer = container_element.createDiv({ attr: { class: "SC-setting-group" } });
        const onStdinChange = async (newStdinContent) => {
            if ("" === newStdinContent) {
                // Set to null
                this.t_shell_command.getConfiguration().input_contents.stdin = null;
            }
            else {
                // Set value
                this.t_shell_command.getConfiguration().input_contents.stdin = newStdinContent;
            }
            await this.plugin.saveSettings();
        };
        new obsidian.Setting(stdinSettingContainer)
            .setDesc("Can contain {{variables}} and/or static text.")
            .addTextArea(textareaComponent => {
            textareaComponent
                .setValue(this.t_shell_command.getInputChannels().stdin ?? "");
            decorateMultilineField(this.plugin, textareaComponent, onStdinChange);
            if (this.plugin.settings.show_autocomplete_menu) {
                // Show autocomplete menu (= a list of available variables).
                createAutocomplete(this.plugin, textareaComponent.inputEl, onStdinChange);
            }
        });
        // Shell command id
        new obsidian.Setting(container_element)
            .setDesc(`Shell command id: ${this.shell_command_id}`)
            .addExtraButton(button => button
            .setIcon("documents")
            .setTooltip(`Copy ${this.shell_command_id} to the clipboard.`)
            .onClick(() => {
            copyToClipboard(this.shell_command_id);
            this.plugin.newNotification(`${this.shell_command_id} was copied to the clipboard.`);
        }));
        if (this.t_shell_command.canAddToCommandPalette()) {
            // Only show Obsidian command palette id if the shell command is available in the command palette.
            const obsidian_command_id = this.t_shell_command.getObsidianCommand().id;
            new obsidian.Setting(container_element)
                .setDesc(`Obsidian command palette id: ${obsidian_command_id}`)
                .addExtraButton(button => button
                .setIcon("documents")
                .setTooltip(`Copy ${obsidian_command_id} to the clipboard.`)
                .onClick(() => {
                copyToClipboard(obsidian_command_id);
                this.plugin.newNotification(`${obsidian_command_id} was copied to the clipboard.`);
            }))
                .settingEl.addClass("SC-no-top-border") // No horizontal ruler between the two id elements.
            ;
        }
    }
    async tabPreactions(container_element) {
        container_element.createEl("p", { text: "Preactions are performed before the actual shell command gets executed, to do certain preparations for the shell command." });
        const preactions_configuration = this.t_shell_command.getConfiguration().preactions;
        // Load config values
        let preaction_prompt_configuration = null;
        for (const preaction_configuration of preactions_configuration) {
            switch (preaction_configuration.type) {
                case "prompt":
                    preaction_prompt_configuration = preaction_configuration;
                    break;
                default:
                    throw new Error("Unrecognised preaction type: " + preaction_configuration.type);
            }
        }
        // Preaction: Prompt
        const prompt_options = {};
        this.plugin.getPrompts().forEach((prompt) => {
            prompt_options[prompt.getID()] = prompt.getTitle();
        });
        let old_selected_prompt_option = (preaction_prompt_configuration?.enabled) ? preaction_prompt_configuration.prompt_id : "no-prompt";
        new obsidian.Setting(container_element)
            .setName("Prompt")
            .setDesc("Prompts are used to ask values from the user right before shell command execution. The values can be accessed in the shell command via custom variables. You can manage all prompts in the plugin's main settings view, under the 'Preactions' tab.")
            .addDropdown(dropdown => dropdown
            .addOption("no-prompt", "No prompt")
            .addOptions(prompt_options)
            .addOption("new", "Create a new prompt")
            .setValue(old_selected_prompt_option)
            .onChange(async (new_prompt_id) => {
            // Create a PreactionPromptConfiguration if it does not exist.
            if (!preaction_prompt_configuration) {
                preaction_prompt_configuration = getDefaultPreaction_Prompt_Configuration();
                preactions_configuration.push(preaction_prompt_configuration);
                this.t_shell_command.resetPreactions();
            }
            // Interpret the selection
            switch (new_prompt_id) {
                case "new": {
                    // Create a new Prompt.
                    const model = getModel(PromptModel.name);
                    const new_prompt = model.newInstance(this.plugin.settings);
                    this.plugin.saveSettings().then(() => {
                        const modal = new PromptSettingsModal(this.plugin, new_prompt, undefined, "Create prompt", async () => {
                            // Prompt is created.
                            dropdown.addOption(new_prompt.getID(), new_prompt.getTitle());
                            dropdown.setValue(new_prompt.getID());
                            preaction_prompt_configuration.enabled = true; // 'as Preaction_Prompt_Configuration' tells TypeScript that the variable is not null.
                            preaction_prompt_configuration.prompt_id = new_prompt.getID();
                            await this.plugin.saveSettings();
                            old_selected_prompt_option = dropdown.getValue();
                        }, async () => {
                            // Prompt creation was cancelled.
                            dropdown.setValue(old_selected_prompt_option); // Reset the dropdown selection.
                            model.deleteInstance(new_prompt);
                            await this.plugin.saveSettings();
                        });
                        modal.open();
                    });
                    break;
                }
                case "no-prompt": {
                    // Disable the prompt.
                    preaction_prompt_configuration.enabled = false;
                    this.t_shell_command.resetPreactions();
                    await this.plugin.saveSettings();
                    old_selected_prompt_option = dropdown.getValue();
                    break;
                }
                default: {
                    // Use an existing prompt.
                    preaction_prompt_configuration.enabled = true;
                    preaction_prompt_configuration.prompt_id = new_prompt_id;
                    await this.plugin.saveSettings();
                    old_selected_prompt_option = dropdown.getValue();
                    break;
                }
            }
        }));
    }
    async tabOutput(container_element) {
        // Output channeling
        const stdout_channel_setting = this.newOutputChannelSetting(container_element, "Output channel for stdout", "stdout");
        this.newOutputChannelSetting(container_element, "Output channel for stderr", "stderr", "If both stdout and stderr use the same channel, stderr will be combined to same message with stdout.");
        // Output wrappers
        this.newOutputWrapperSetting(container_element, "Output wrapper for stdout", "stdout", "Output wrappers can be used to surround output with predefined text, e.g. to put output into a code block. Note: If 'Output mode' is 'Realtime', wrappers will probably appear multiple times in output!");
        this.newOutputWrapperSetting(container_element, "Output wrapper for stderr", "stderr");
        // ANSI code conversion.
        this.newAnsiCodeConversionSetting(container_element, obsidian.sanitizeHTMLToDom("Shell programs may output <a href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">ANSI code</a> to apply <span style=\"color: magenta\">colors</span>, <strong>font</strong> <em>styles</em> and other formatting (e.g. links) to the outputted text. If turned on, possible ANSI code occurrences are converted to HTML elements using <a href=\"https://github.com/drudru/ansi_up\">ansi_up.js</a> library (bundled in this plugin). Otherwise, possible ANSI code is displayed as-is, which may look ugly."));
        // Output handling mode
        new obsidian.Setting(container_element)
            .setName("Output handling mode")
            .setDesc("Set to 'Realtime' if your shell command runs for a long time AND you want output handling to start as soon as any outputted content is available. Output channels might be used multiple times during a single process. 'Wait until finished' postpones output handling until all output is received, and handles it as a single bunch. If uncertain, use the traditional 'Wait until finished'.")
            .addDropdown(dropdown => dropdown
            .addOptions({
            "buffered": "Wait until finished",
            "realtime": "Realtime (experimental)",
        })
            .setValue(this.t_shell_command.getConfiguration().output_handling_mode)
            .onChange(async (newMode) => {
            this.t_shell_command.getConfiguration().output_handling_mode = newMode;
            await this.plugin.saveSettings();
        }))
            // Documentation link
            .addExtraButton(icon => icon
            .setIcon("help")
            .onClick(() => gotoURL(Documentation.outputHandling.outputHandlingMode))
            .setTooltip("Documentation: Output handling mode"));
        // Order of output channels
        new obsidian.Setting(container_element)
            .setName("Order of stdout/stderr output")
            .setDesc("When output contains both errors and normal output, which one should be presented first? (Only matters if 'Output handling' is 'Wait until finished').")
            .addDropdown(dropdown => dropdown
            .addOptions({
            "stdout-first": "Stdout first, then stderr.",
            "stderr-first": "Stderr first, then stdout.",
        })
            .setValue(this.t_shell_command.getOutputChannelOrder())
            .onChange(async (value) => {
            this.t_shell_command.getConfiguration().output_channel_order = value;
            await this.plugin.saveSettings();
        }));
        // Focus on the stdout channel dropdown field
        stdout_channel_setting.controlEl.find("select").addClass("SC-focus-element-on-tab-opening");
        // Ignore errors field
        new obsidian.Setting(container_element)
            .setName("Ignore error codes")
            .setDesc("A comma separated list of numbers. If executing a shell command fails with one of these exit codes, no error message will be displayed, and the above stderr channel will be ignored. Stdout channel will still be used for stdout. Error codes must be integers and greater than or equal to 0. Anything else will be removed. Note: If 'Output handling' is 'Realtime', no exit code based ignoring can be done, as an error code is only received when a shell command process finishes.")
            .addText(text => text
            .setValue(this.t_shell_command.getIgnoreErrorCodes().join(","))
            .onChange(async (value) => {
            // Parse the string of comma separated numbers
            const ignore_error_codes = [];
            const raw_error_codes = value.split(",");
            for (const i in raw_error_codes) {
                const raw_error_code = raw_error_codes[i];
                const error_code_candidate = parseInt(raw_error_code.trim()); // E.g. an empty string converts to NaN (= Not a Number).
                // Ensure that the error code is not NaN, 0 or a negative number.
                if (!isNaN(error_code_candidate) && error_code_candidate >= 0) {
                    // The candidate is legit.
                    ignore_error_codes.push(error_code_candidate);
                }
            }
            // Save the validated error numbers
            this.t_shell_command.getConfiguration().ignore_error_codes = ignore_error_codes;
            await this.plugin.saveSettings();
            // Update icon
            const icon_container = this.name_setting.nameEl.find("span.shell-commands-ignored-error-codes-icon-container");
            if (this.t_shell_command.getIgnoreErrorCodes().length) {
                // Show icon
                icon_container.setAttr("aria-label", generateIgnoredErrorCodesIconTitle(this.t_shell_command.getIgnoreErrorCodes()));
                icon_container.removeClass("SC-hide");
            }
            else {
                // Hide icon
                icon_container.addClass("SC-hide");
            }
        }));
        // "Show a notification when executing shell commands" field
        createExecutionNotificationField(container_element, this.t_shell_command.getConfiguration().execution_notification_mode, this.plugin.settings.execution_notification_mode, this.plugin.settings.notification_message_duration, async (newExecutionNotificationMode) => {
            // Save the change.
            this.t_shell_command.getConfiguration().execution_notification_mode = newExecutionNotificationMode;
            await this.plugin.saveSettings();
        });
    }
    async tabEnvironments(container_element) {
        // Default shell command for platforms that don't have a specific command.
        const defaultSettingGroup = this.newDefaultShellCommandContentSetting(container_element, () => {
            // When the default shell command content changes, update placeholders of platform specific shell command fields.
            for (const settingGroup of platformSpecificSettingGroups.values()) {
                const textareaComponent = settingGroup.shell_command_setting.components.first();
                if (textareaComponent) {
                    textareaComponent.setPlaceholder(this.t_shell_command.getDefaultShellCommand());
                    textareaComponent.onChanged(); // Update textarea dimensions.
                }
            }
            // Update the shell command content on the main settings modal.
            const mainModalShellCommandTextareaComponent = this.setting_tab.setting_groups[this.shell_command_id].shell_command_setting.components.first();
            if (mainModalShellCommandTextareaComponent) {
                mainModalShellCommandTextareaComponent.setValue(this.t_shell_command.getDefaultShellCommand());
                mainModalShellCommandTextareaComponent.onChanged(); // Update textarea dimensions.
            }
        });
        // Platform specific shell commands
        let platform_id;
        const platformSpecificSettingGroups = new Map;
        for (platform_id in PlatformNames) {
            platformSpecificSettingGroups.set(platform_id, createPlatformSpecificShellCommandField(this.plugin, container_element, this.t_shell_command, platform_id, this.plugin.settings.show_autocomplete_menu, () => {
                // When whichever platform specific shell command content changes, update default shell command's preview, because its shell might change.
                const defaultShell = this.t_shell_command.getShellForDefaultCommand();
                defaultSettingGroup.refreshPreview(defaultShell);
            }));
        }
        // Platform specific shell selection
        createShellSelectionFields(this.plugin, container_element, this.t_shell_command.getShells(), false, (platformId) => {
            // When a shell is changed, update previews of default and platform specific shell command fields.
            const shellForDefaultCommand = this.t_shell_command.getShellForDefaultCommand();
            defaultSettingGroup.refreshPreview(shellForDefaultCommand);
            this.settingGroupInMainTab.refreshPreview(shellForDefaultCommand);
            platformSpecificSettingGroups.get(platformId)?.refreshPreview(this.t_shell_command.getShellForPlatform(platformId));
        });
    }
    async tabEvents(container_element) {
        // Command palette
        const command_palette_availability_setting = new obsidian.Setting(container_element)
            .setName("Availability in Obsidian's command palette")
            .addDropdown(dropdown => dropdown
            .addOptions(CommandPaletteOptions)
            .setValue(this.t_shell_command.getConfiguration().command_palette_availability)
            .onChange(async (value) => {
            // Store value
            this.t_shell_command.getConfiguration().command_palette_availability = value;
            // Update command palette
            if (this.t_shell_command.canAddToCommandPalette()) {
                // Register to command palette
                this.t_shell_command.registerToCommandPalette();
            }
            else {
                // Unregister from command palette
                this.t_shell_command.unregisterFromCommandPalette();
            }
            // Save
            await this.plugin.saveSettings();
        }));
        // Debouncing
        // TODO: Extract to a separate method. Actually, consider creating subclasses for each tab, e.g. ShellCommandSettingsModal_TabEvents. Then it's more meaningful to create new tab specific methods.
        const shellCommandConfiguration = this.t_shell_command.getConfiguration();
        const noDebounceIcon = "shield-ban";
        const debouncingSettingsContainer = container_element.createDiv({ cls: "SC-setting-group" });
        new obsidian.Setting(debouncingSettingsContainer)
            .setName("Debouncing (experimental)")
            .setHeading()
            .setDesc("If enabled, events cannot perform multiple concurrent (or too adjacent) executions of this shell command. Debouncing does not affect events marked with ")
            .addExtraButton(helpButton => helpButton
            .setIcon("help")
            .setTooltip("Documentation: Events - Debouncing")
            .onClick(() => gotoURL(Documentation.events.debouncing)))
            .then((setting) => {
            // Append no debouncing icon and a dot to description.
            obsidian.setIcon(setting.descEl.createSpan(), noDebounceIcon);
            setting.descEl.innerHTML += ".";
        });
        new obsidian.Setting(debouncingSettingsContainer)
            .setName("Execute before cooldown")
            .addToggle(executeEarlyToggle => executeEarlyToggle
            .setValue(shellCommandConfiguration.debounce?.executeEarly ?? false)
            .onChange((executeEarly) => {
            if (null === shellCommandConfiguration.debounce) {
                shellCommandConfiguration.debounce = Debouncer.getDefaultConfiguration(executeEarly, false);
            }
            else {
                shellCommandConfiguration.debounce.executeEarly = executeEarly;
            }
            possiblyCleanupDebounceConfiguration();
            defineDebounceAdditionalSettings();
            this.plugin.saveSettings();
        }));
        new obsidian.Setting(debouncingSettingsContainer)
            .setName("Execute after cooldown")
            .addToggle(executeLateToggle => executeLateToggle
            .setValue(shellCommandConfiguration.debounce?.executeLate ?? false)
            .onChange((executeLate) => {
            if (null === shellCommandConfiguration.debounce) {
                shellCommandConfiguration.debounce = Debouncer.getDefaultConfiguration(false, executeLate);
            }
            else {
                shellCommandConfiguration.debounce.executeLate = executeLate;
            }
            possiblyCleanupDebounceConfiguration();
            defineDebounceAdditionalSettings();
            this.plugin.saveSettings();
        }));
        const debounceAdditionalSettingsContainer = debouncingSettingsContainer.createDiv();
        const defineDebounceAdditionalSettings = () => {
            debounceAdditionalSettingsContainer.innerHTML = ""; // Remove possible earlier settings.
            if (this.t_shell_command.isDebouncingEnabled()) {
                // Debouncing is enabled.
                // Description for debouncing.
                let debouncingDescription;
                switch ((shellCommandConfiguration.debounce?.executeEarly ? "early" : "") + "-" + (shellCommandConfiguration.debounce?.executeLate ? "late" : "")) {
                    case "early-late":
                        // Both early and late execution.
                        debouncingDescription = "When executing both <em>Before and After cooldown</em>, the shell command is executed right-away, and <strong>subsequent executions are postponed</strong> for as long as the execution is in progress, <strong>plus</strong> the <em>Cooldown duration</em> after the execution. After the cooldown period ends, the shell command is possibly re-executed, if any subsequent executions were postponed.";
                        break;
                    case "early-":
                        // Early execution.
                        debouncingDescription = "When executing <em>Before cooldown</em>, the shell command is executed right-away, and <strong>subsequent executions are prevented</strong> for as long as the execution is in progress, <strong>plus</strong> the <em>Cooldown duration</em> after the execution.";
                        break;
                    case "-late":
                        // Late execution.
                        debouncingDescription = "When executing <em>After cooldown</em>, the shell command execution will be delayed by the <em>Cooldown duration</em>. <strong>Subsequent executions are prevented</strong> during the cooldown phase, or <strong>postponed</strong> during the execution phase.";
                        break;
                    default:
                        throw new Error("Unidentified debouncing state: " + JSON.stringify(shellCommandConfiguration.debounce));
                }
                const debouncingDescriptionFragment = new DocumentFragment();
                debouncingDescriptionFragment.createDiv().innerHTML = debouncingDescription;
                new obsidian.Setting(debounceAdditionalSettingsContainer)
                    .setClass("SC-full-description")
                    .setDesc(debouncingDescriptionFragment);
                // Cooldown duration setting.
                new obsidian.Setting(debounceAdditionalSettingsContainer)
                    .setName("Cooldown duration (seconds)")
                    .setDesc("If you only need to prevent simultaneous execution, but do not need extra cooldown time, you can set this to 0.")
                    .addText(thresholdTextComponent => thresholdTextComponent
                    .setValue((shellCommandConfiguration.debounce?.cooldownDuration ?? 0).toString())
                    .onChange((newThresholdString) => {
                    const newThreshold = inputToFloat(newThresholdString, 1);
                    if (!shellCommandConfiguration.debounce) {
                        throw new Error("shellCommandConfiguration.debounce is falsy.");
                    }
                    if (newThreshold >= 0) {
                        shellCommandConfiguration.debounce.cooldownDuration = newThreshold;
                    }
                    else {
                        shellCommandConfiguration.debounce.cooldownDuration = 0;
                    }
                    this.plugin.saveSettings();
                }));
                const prolongCooldownDescriptionFragment = new DocumentFragment();
                prolongCooldownDescriptionFragment.createDiv().innerHTML = "If enabled, events occurring during a <strong>cooldown</strong> phase will reset the cooldown timer, making the cooldown last longer. Thus, executions are avoided for an extended period.";
                new obsidian.Setting(debounceAdditionalSettingsContainer)
                    .setName("Prolong cooldown")
                    .setDesc(prolongCooldownDescriptionFragment)
                    .setClass("SC-full-description")
                    .addToggle(prolongCooldownToggle => prolongCooldownToggle
                    .setValue(shellCommandConfiguration.debounce?.prolongCooldown ?? false)
                    .onChange((newProlongCooldown) => {
                    if (!shellCommandConfiguration.debounce) {
                        throw new Error("shellCommandConfiguration.debounce is falsy.");
                    }
                    shellCommandConfiguration.debounce.prolongCooldown = newProlongCooldown;
                    this.plugin.saveSettings();
                }));
            }
            else {
                // Debouncing is disabled.
                new obsidian.Setting(debounceAdditionalSettingsContainer)
                    .setDesc("Debouncing is disabled, so executions can happen simultaneously and without delays.");
            }
        };
        const possiblyCleanupDebounceConfiguration = () => {
            if (shellCommandConfiguration.debounce) {
                if (!shellCommandConfiguration.debounce.executeEarly && !shellCommandConfiguration.debounce.executeLate) {
                    // Debouncing is disabled, but the configuration object exists.
                    if (shellCommandConfiguration.debounce.cooldownDuration === 0) {
                        // The DebounceConfiguration object can be removed. No need to remember the cooldown, because user probably did not type 0 themselves.
                        shellCommandConfiguration.debounce = null;
                        this.t_shell_command.resetDebouncer(); // If debouncing is re-enabled later, ensure there's no old debouncer trying to use an old, stale configuration.
                    }
                }
            }
        };
        defineDebounceAdditionalSettings();
        // Focus on the command palette availability field
        command_palette_availability_setting.controlEl.find("select").addClass("SC-focus-element-on-tab-opening");
        // Events
        new obsidian.Setting(container_element)
            .setName("Execute this shell command automatically when:")
            .setHeading() // Make the name bold
        ;
        getSC_Events(this.plugin).forEach((sc_event) => {
            const is_event_enabled = this.t_shell_command.isSC_EventEnabled(sc_event.static().getCode());
            const setting = new obsidian.Setting(container_element)
                .setName(sc_event.static().getTitle())
                .addToggle(toggle => toggle
                .setValue(is_event_enabled)
                .onChange(async (enable) => {
                if (enable) {
                    // Enable the event
                    this.t_shell_command.enableSC_Event(sc_event);
                    extra_settings_container.style.display = "block"; // Show extra settings
                }
                else {
                    // Disable the event
                    this.t_shell_command.disableSC_Event(sc_event);
                    extra_settings_container.style.display = "none"; // Hide extra settings
                }
                // Save
                await this.plugin.saveSettings();
            }))
                // Documentation icon
                .addExtraButton(icon => icon
                .setIcon("help")
                .onClick(() => gotoURL(sc_event.static().getDocumentationLink()))
                .setTooltip("Documentation: " + sc_event.static().getTitle() + " event"));
            // Mention additional variables (if any)
            if (sc_event.createSummaryOfEventVariables(setting.descEl)) {
                setting.descEl.insertAdjacentText("afterbegin", "Additional variables: ");
            }
            // Create a no debouncing icon, if applicable.
            if (!sc_event.static().canDebounce()) {
                setting.nameEl.insertAdjacentText("beforeend", " ");
                const iconSpan = setting.nameEl.createSpan();
                obsidian.setIcon(iconSpan, noDebounceIcon);
                iconSpan.setAttr("aria-label", "This event cannot be limited by debouncing.");
            }
            // Extra settings
            const extra_settings_container = container_element.createDiv();
            extra_settings_container.style.display = is_event_enabled ? "block" : "none";
            sc_event.createExtraSettingsFields(extra_settings_container, this.t_shell_command);
        });
    }
    async tabVariables(containerElement) {
        // Default values for variables
        new obsidian.Setting(containerElement)
            .setName("Default values for variables")
            .setDesc("Certain variables can be inaccessible during certain situations, e.g. {{file_name}} is not available when no file pane is focused. You can define default values that will be used when a variable is otherwise unavailable.")
            .setHeading();
        createVariableDefaultValueFields(this.plugin, containerElement, this.t_shell_command);
    }
    activateTab(tab_id) {
        if (undefined === this.tab_structure.buttons[tab_id]) {
            throw Error("Invalid tab id: " + tab_id);
        }
        this.tab_structure.buttons[tab_id].click();
    }
    newOutputChannelSetting(container_element, title, output_stream_name, description = "") {
        const output_channel_options = getOutputChannelsOptionList(output_stream_name);
        return new obsidian.Setting(container_element)
            .setName(title)
            .setDesc(description)
            .addDropdown(dropdown => dropdown
            .addOptions(output_channel_options)
            .setValue(this.t_shell_command.getOutputHandlers()[output_stream_name].handler)
            .onChange(async (value) => {
            this.t_shell_command.getConfiguration().output_handlers[output_stream_name].handler = value;
            await this.plugin.saveSettings();
        }));
    }
    newOutputWrapperSetting(container_element, title, output_stream_name, description = "") {
        const output_wrapper_options = {};
        this.plugin.getOutputWrappers().forEach((output_wrapper) => {
            output_wrapper_options[output_wrapper.getID()] = output_wrapper.getTitle();
        });
        const output_wrappers = this.t_shell_command.getConfiguration().output_wrappers;
        let old_selected_output_wrapper_option = output_wrappers[output_stream_name] ?? "no-output-wrapper";
        return new obsidian.Setting(container_element)
            .setName(title)
            .setDesc(description)
            .addDropdown(dropdown_component => dropdown_component
            .addOption("no-output-wrapper", "No " + output_stream_name + " wrapper")
            .addOptions(output_wrapper_options)
            .addOption("new", "Create a new output wrapper")
            .setValue(old_selected_output_wrapper_option)
            .onChange(async (output_wrapper_id) => {
            switch (output_wrapper_id) {
                case "new": {
                    // Create a new OutputWrapper.
                    const output_wrapper_model = getModel(OutputWrapperModel.name);
                    const new_output_wrapper = output_wrapper_model.newInstance(this.plugin.settings);
                    this.plugin.saveSettings().then(() => {
                        const modal = new OutputWrapperSettingsModal(this.plugin, new_output_wrapper, undefined, "Create output wrapper", async () => {
                            // Output wrapper is created.
                            dropdown_component.addOption(new_output_wrapper.getID(), new_output_wrapper.getTitle());
                            dropdown_component.setValue(new_output_wrapper.getID());
                            output_wrappers[output_stream_name] = new_output_wrapper.getID();
                            await this.plugin.saveSettings();
                            old_selected_output_wrapper_option = dropdown_component.getValue();
                        }, async () => {
                            // Prompt creation was cancelled.
                            dropdown_component.setValue(old_selected_output_wrapper_option); // Reset the dropdown selection.
                            output_wrapper_model.deleteInstance(new_output_wrapper);
                            await this.plugin.saveSettings();
                        });
                        modal.open();
                    });
                    break;
                }
                case "no-output-wrapper": {
                    // Disable output wrapper.
                    output_wrappers[output_stream_name] = null;
                    await this.plugin.saveSettings();
                    break;
                }
                default: {
                    // Use an existing output wrapper.
                    output_wrappers[output_stream_name] = output_wrapper_id;
                    await this.plugin.saveSettings();
                    break;
                }
            }
        }));
    }
    newAnsiCodeConversionSetting(containerElement, description) {
        const ansiCodeSetting = new obsidian.Setting(containerElement)
            .setName("Detect colors, font styles etc. in output (ANSI code)")
            .setDesc(description);
        const addAnsiCodeFieldForOutputStream = (outputStreamName) => {
            ansiCodeSetting.addDropdown(dropdownComponent => dropdownComponent
                .addOptions({
                enable: outputStreamName + ": Enable",
                disable: outputStreamName + ": Disable",
            })
                .setValue(this.t_shell_command.getConfiguration().output_handlers[outputStreamName].convert_ansi_code ? "enable" : "disable")
                .onChange(async (enableString) => {
                this.t_shell_command.getConfiguration().output_handlers[outputStreamName].convert_ansi_code = (enableString === "enable");
                await this.plugin.saveSettings();
            }));
        };
        addAnsiCodeFieldForOutputStream("stdout");
        addAnsiCodeFieldForOutputStream("stderr");
        ansiCodeSetting.addExtraButton(helpButton => helpButton
            .setIcon("help")
            .setTooltip("Documentation: Output text styling with ANSI code")
            .onClick(() => gotoURL(Documentation.outputHandling.ansiCode)));
    }
    newDefaultShellCommandContentSetting(containerElement, onChange) {
        const settingGroup = CreateShellCommandFieldCore(this.plugin, containerElement, "Default shell command", this.t_shell_command.getPlatformSpecificShellCommands().default, this.t_shell_command.getShellForDefaultCommand() ?? this.plugin.getDefaultShell(), // If default shell command content is never used, just get some shell.
        this.t_shell_command, this.plugin.settings.show_autocomplete_menu, async (shellCommandContent) => {
            // Store the updated shell command content.
            this.t_shell_command.getPlatformSpecificShellCommands().default = shellCommandContent; // Can be an empty string.
            await this.plugin.saveSettings();
            onChange();
        });
        settingGroup.name_setting.setDesc("Used on operating systems that do not define their own shell command.");
        // Focus on the textarea.
        settingGroup.shell_command_setting.controlEl.find("textarea").addClass("SC-focus-element-on-tab-opening");
        return settingGroup;
    }
    approve() {
        // No need to perform any action, just close the modal.
        this.close();
    }
}
ShellCommandSettingsModal.GENERAL_OPTIONS_SUMMARY = "Alias, Icon, Confirmation, Stdin";
ShellCommandSettingsModal.PREACTIONS_OPTIONS_SUMMARY = "Preactions: Prompt for asking values from user";
ShellCommandSettingsModal.OUTPUT_OPTIONS_SUMMARY = "Stdout/stderr handling, Ignore errors";
ShellCommandSettingsModal.ENVIRONMENTS_OPTIONS_SUMMARY = "Shell selection, Operating system specific shell commands";
ShellCommandSettingsModal.EVENTS_SUMMARY = "Events";
ShellCommandSettingsModal.VARIABLES_SUMMARY = "Default values for variables";

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * TODO: Rename to DeleteShellCommandModal
 */
class DeleteModal extends SC_Modal {
    constructor(plugin, shell_command_id, setting_group, shell_command_element) {
        super(plugin);
        this.shell_command_id = shell_command_id;
        this.t_shell_command = plugin.getTShellCommands()[shell_command_id];
        this.setting_group = setting_group;
        this.shell_command_element = shell_command_element;
    }
    onOpen() {
        super.onOpen();
        this.setTitle("Delete: " + this.t_shell_command.getShellCommandContent());
        if (this.t_shell_command.getAlias()) {
            this.modalEl.createEl("p", { text: "Alias: " + this.t_shell_command.getAlias() });
        }
        this.modalEl.createEl("p", { text: "Are you sure you want to delete this shell command?" });
        const delete_button = this.modalEl.createEl("button", { text: "Yes, delete" });
        delete_button.onclick = async () => this.approve();
    }
    async approve() {
        // Unregister possible events in order to prevent them becoming ghosts that just keep executing even after removing the configuration.
        this.t_shell_command.unregisterSC_Events();
        // Remove the command
        debugLog("Command " + this.shell_command_id + " gonna be removed.");
        this.t_shell_command.unregisterFromCommandPalette(); // Remove from the command palette.
        delete this.plugin.getTShellCommands()[this.shell_command_id]; // Remove the TShellCommand object.
        // Remove from the plugin's settings.
        const shellCommandIndex = this.plugin.getShellCommandConfigurationIndex(this.shell_command_id);
        // Index probably always exists, but check just in case. Will make TypeScript compiler happy. :)
        if (undefined == shellCommandIndex) {
            throw new Error("Shell command deletion failed. Did not get shell command index in settings container.");
        }
        this.plugin.settings.shell_commands.splice(shellCommandIndex, 1);
        // Remove the setting fields
        this.shell_command_element.remove();
        await this.plugin.saveSettings();
        debugLog("Command removed.");
        this.close();
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * Creates a shell command textarea (by calling createShellCommandFieldCore()) and related control buttons.
 *
 * TODO: Rename this function to createShellCommandSettings() and createShellCommandFieldCore() to createShellCommandTextarea().
 *
 * @param plugin
 * @param container_element
 * @param setting_tab
 * @param shell_command_id Either an alphanumeric string or "new" if it's a field for a command that does not exist yet.
 * @param show_autocomplete_menu
 * @param onAfterPreviewGenerated
 */
function createShellCommandField(plugin, container_element, setting_tab, shell_command_id, show_autocomplete_menu, onAfterPreviewGenerated) {
    const is_new = "new" === shell_command_id;
    let t_shell_command;
    if (is_new) {
        // Create an empty command
        t_shell_command = plugin.newTShellCommand();
        shell_command_id = t_shell_command.getId(); // Replace "new" with a real id.
    }
    else {
        // Use an old shell command
        t_shell_command = plugin.getTShellCommands()[shell_command_id];
    }
    debugLog("Create command field for command #" + shell_command_id + (is_new ? " (NEW)" : ""));
    let shell_command;
    if (is_new) {
        shell_command = "";
    }
    else {
        shell_command = t_shell_command.getDefaultShellCommand();
    }
    // Wrap all shell command setting elements in a single div.
    const shell_command_element = container_element.createDiv();
    shell_command_element.addClass("SC-id-" + shell_command_id);
    const setting_group = CreateShellCommandFieldCore(plugin, shell_command_element, generateShellCommandFieldIconAndName(t_shell_command), shell_command, t_shell_command.getShellForDefaultCommand() ?? plugin.getDefaultShell(), // If default shell command content is newer used, just get some shell.
    t_shell_command, show_autocomplete_menu, async (shell_command) => {
        if (is_new) {
            debugLog("Creating new command " + shell_command_id + ": " + shell_command);
        }
        else {
            debugLog("Command " + shell_command_id + " gonna change to: " + shell_command);
        }
        // Do this in both cases, when creating a new command and when changing an old one:
        t_shell_command.getConfiguration().platform_specific_commands.default = shell_command;
        if (is_new) {
            // Create a new command
            // plugin.registerShellCommand(t_shell_command); // I don't think this is needed to be done anymore
            debugLog("Command created.");
        }
        else {
            // Change an old command
            t_shell_command.renameObsidianCommand(t_shell_command.getAliasOrShellCommand()); // Change the command's name in Obsidian's command palette and in hotkey settings.
            debugLog("Command changed.");
        }
        await plugin.saveSettings();
    }, onAfterPreviewGenerated);
    setting_tab.setting_groups[shell_command_id] = setting_group;
    // Primary icon buttons
    createExecuteNowButton(plugin, setting_group.name_setting, t_shell_command);
    setting_group.name_setting
        .addExtraButton(button => button
        .setTooltip(ShellCommandSettingsModal.GENERAL_OPTIONS_SUMMARY)
        .onClick(async () => {
        // Open an extra options modal: General tab
        const modal = new ShellCommandSettingsModal(plugin, shell_command_id, setting_tab);
        modal.open();
        modal.activateTab("extra-options-general");
    }))
        .addExtraButton(button => button
        .setTooltip(ShellCommandSettingsModal.PREACTIONS_OPTIONS_SUMMARY)
        .setIcon("note-glyph")
        .onClick(async () => {
        // Open an extra options modal: Preactions tab
        const modal = new ShellCommandSettingsModal(plugin, shell_command_id, setting_tab);
        modal.open();
        modal.activateTab("extra-options-preactions");
    }))
        .addExtraButton(button => button
        .setTooltip(ShellCommandSettingsModal.OUTPUT_OPTIONS_SUMMARY)
        .setIcon("lines-of-text")
        .onClick(async () => {
        // Open an extra options modal: Output tab
        const modal = new ShellCommandSettingsModal(plugin, shell_command_id, setting_tab);
        modal.open();
        modal.activateTab("extra-options-output");
    }))
        .addExtraButton(button => button
        .setTooltip(ShellCommandSettingsModal.ENVIRONMENTS_OPTIONS_SUMMARY)
        .setIcon("stacked-levels")
        .onClick(async () => {
        // Open an extra options modal: Environments tab
        const modal = new ShellCommandSettingsModal(plugin, shell_command_id, setting_tab);
        modal.open();
        modal.activateTab("extra-options-environments");
    }))
        .addExtraButton(button => button
        .setTooltip(ShellCommandSettingsModal.EVENTS_SUMMARY)
        .setIcon("dice")
        .onClick(async () => {
        // Open an extra options modal: Events tab
        const modal = new ShellCommandSettingsModal(plugin, shell_command_id, setting_tab);
        modal.open();
        modal.activateTab("extra-options-events");
    }))
        .addExtraButton(button => button
        .setTooltip(ShellCommandSettingsModal.VARIABLES_SUMMARY)
        .setIcon("code-glyph")
        .onClick(async () => {
        // Open an extra options modal: Variables tab
        const modal = new ShellCommandSettingsModal(plugin, shell_command_id, setting_tab);
        modal.open();
        modal.activateTab("extra-options-variables");
    }))
        .addExtraButton(button => button
        .setTooltip("Delete this shell command")
        .setIcon("trash")
        .onClick(async () => {
        // Open a delete modal
        const modal = new DeleteModal(plugin, shell_command_id, setting_group, shell_command_element);
        modal.open();
    }));
    // Informational icons (= non-clickable)
    const icon_container = setting_group.name_setting.nameEl.createEl("span", { attr: { class: "SC-main-icon-container" } });
    // "Ask confirmation" icon.
    const confirm_execution_icon_container = icon_container.createEl("span", { attr: { "aria-label": "Asks confirmation before execution.", class: "shell-commands-confirm-execution-icon-container" } });
    obsidian.setIcon(confirm_execution_icon_container, "languages");
    if (!t_shell_command.getConfirmExecution()) {
        // Do not display the icon for commands that do not use confirmation.
        confirm_execution_icon_container.addClass("SC-hide");
    }
    // "Ignored error codes" icon
    const ignored_error_codes_icon_container = icon_container.createEl("span", { attr: { "aria-label": generateIgnoredErrorCodesIconTitle(t_shell_command.getIgnoreErrorCodes()), class: "shell-commands-ignored-error-codes-icon-container" } });
    obsidian.setIcon(ignored_error_codes_icon_container, "strikethrough-glyph");
    if (!t_shell_command.getIgnoreErrorCodes().length) {
        // Do not display the icon for commands that do not ignore any errors.
        ignored_error_codes_icon_container.addClass("SC-hide");
    }
    // Secondary icon buttons
    setting_group.preview_setting.addExtraButton(button => button
        .setIcon("link")
        .setTooltip("Copy this shell command's Obsidian URI to the clipboard. Visiting the URI executes the shell command. " + CmdOrCtrl() + " + click: Copy a markdown link.")
        // onClick() handler - use a custom one instead of ExtraButtonComponent.onClick(), because Obsidian API (at least v. 0.14.8) does not support detecting CTRL press. https://forum.obsidian.md/t/fr-settings-pass-mouseevent-to-extrabuttoncomponent-onclick/37177
        .extraSettingsEl.addEventListener("click", (event) => {
        const ctrl_clicked = isCmdOrCtrlPressed(event);
        const execution_uri = t_shell_command.getExecutionURI();
        let result;
        if (ctrl_clicked) {
            // A full link is wanted.
            result = `[${escapeMarkdownLinkCharacters(t_shell_command.getAlias())}](${escapeMarkdownLinkCharacters(execution_uri)})`;
        }
        else {
            // Only the URI is wanted.
            result = execution_uri;
        }
        copyToClipboard(result);
        plugin.newNotification("Copied to clipboard: " + os.EOL + result);
    }));
    // Create Hotkey settings icon.
    if (t_shell_command.canHaveHotkeys()) {
        setting_group.preview_setting.addExtraButton(button => button
            .setIcon("any-key")
            .setTooltip("Go to hotkey settings.")
            .onClick(() => {
            // The most important parts of this closure function are copied 2022-04-27 from https://github.com/pjeby/hotkey-helper/blob/c8a032e4c52bd9ce08cb909cec15d1ed9d0a3439/src/plugin.js#L436-L442 (also from other lines of the same file).
            // @ts-ignore This is PRIVATE API access. Not good, but then again the feature is not crucial - if it breaks, it won't interrupt anything important.
            plugin.app.setting?.openTabById("hotkeys");
            // @ts-ignore
            const hotkeys_settings_tab = plugin.app.setting.settingTabs.filter(tab => tab.id === "hotkeys").shift();
            const searchErrorMessage = "Shell command hotkey search failed due to a private API change in the hotkey search. Please start a discussion in the SC plugin's GitHub repo.";
            if (hotkeys_settings_tab) {
                const hotkeySearchElement = hotkeys_settings_tab.searchInputEl ?? // For Obsidian versions before 1.2.0.
                    hotkeys_settings_tab.searchComponent.inputEl // For Obsidian version 1.2.0 and onwards.
                ;
                if (hotkeySearchElement && hotkeys_settings_tab.updateHotkeyVisibility) {
                    debugLog("Hotkeys: Filtering by shell command " + t_shell_command.getObsidianCommand().name);
                    hotkeySearchElement.value = t_shell_command.getObsidianCommand().name;
                    hotkeys_settings_tab.updateHotkeyVisibility();
                }
                else {
                    debugLog("Hotkeys: Cannot do filtering due to API changes. Search element has changed.");
                    this.plugin.newNotification(searchErrorMessage);
                }
            }
            else {
                debugLog("Hotkeys: Cannot do filtering due to API changes. 'Hotkeys' settings tab has changed.");
                this.plugin.newNotification(searchErrorMessage);
            }
        }));
    }
    // Add hotkey information
    if (!is_new && t_shell_command.canHaveHotkeys()) {
        const hotkeys = getHotkeysForShellCommand(plugin, shell_command_id);
        if (hotkeys) {
            let hotkeys_joined = "";
            hotkeys.forEach((hotkey) => {
                if (hotkeys_joined) {
                    hotkeys_joined += "<br>";
                }
                hotkeys_joined += HotkeyToString(hotkey);
            });
            const hotkey_div = setting_group.preview_setting.controlEl.createEl("div", { attr: { class: "setting-item-description SC-hotkey-info" } });
            // Comment out the icon because it would look like a clickable button (as there are other clickable icons in the settings).
            // setIcon(hotkey_div, "any-key", 22); // Hotkey icon
            hotkey_div.insertAdjacentHTML("beforeend", " " + hotkeys_joined);
        }
    }
    debugLog("Created.");
}
/**
 * @param t_shell_command
 * @public Exported because ShellCommandExtraOptionsModal uses this too.
 */
function generateShellCommandFieldIconAndName(t_shell_command) {
    const icon_html = t_shell_command.getIconHTML() + " ";
    if (t_shell_command.getAlias()) {
        return icon_html + t_shell_command.getAlias();
    }
    return icon_html + "Shell command without alias";
}
/**
 * @param ignored_error_codes
 * @public Exported because ShellCommandExtraOptionsModal uses this too.
 */
function generateIgnoredErrorCodesIconTitle(ignored_error_codes) {
    const plural = ignored_error_codes.length !== 1 ? "s" : "";
    return "Ignored error" + plural + ": " + ignored_error_codes.join(",");
}
function createExecuteNowButton(plugin, setting, t_shell_command) {
    setting.addExtraButton(button => button
        .setTooltip("Normal click: Execute now. " + CmdOrCtrl() + " + click: Execute and ask what to do with output.")
        .setIcon("run-command")
        .extraSettingsEl.addEventListener("click", async (event) => {
        const ctrl_clicked = isCmdOrCtrlPressed(event);
        const parsing_process = t_shell_command.createParsingProcess(null); // No SC_Event is available when executing shell commands manually.
        if (await parsing_process.process()) {
            const executor = new ShellCommandExecutor(plugin, t_shell_command, null); // No SC_Event is available when manually executing the shell command.
            await executor.doPreactionsAndExecuteShellCommand(parsing_process, ctrl_clicked ? "modal" : undefined);
        }
        else {
            parsing_process.displayErrorMessages();
        }
    }));
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
/**
 * TODO: Rename to MainSettingsModal. Then it better in line with ShellCommandSettingsModal.
 */
class SC_MainSettingsTab extends obsidian.PluginSettingTab {
    constructor(app, plugin) {
        super(app, plugin);
        this.setting_groups = {};
        this.last_position = {
            scroll_position: 0,
            tab_name: "main-shell-commands",
        };
        this.plugin = plugin;
    }
    display() {
        const { containerEl } = this;
        containerEl.empty();
        this.tab_structure = createTabs(containerEl, {
            "main-shell-commands": {
                title: "Shell commands",
                icon: "run-command",
                content_generator: (container_element) => this.tabShellCommands(container_element),
            },
            "main-environments": {
                title: "Environments",
                icon: "stacked-levels",
                content_generator: (container_element) => this.tabEnvironments(container_element),
            },
            "main-preactions": {
                title: "Preactions",
                icon: "note-glyph",
                content_generator: (container_element) => this.tabPreactions(container_element),
            },
            "main-output": {
                title: "Output",
                icon: "lines-of-text",
                content_generator: (container_element) => this.tabOutput(container_element),
            },
            "main-events": {
                title: "Events",
                icon: "dice",
                content_generator: (container_element) => this.tabEvents(container_element),
            },
            "main-variables": {
                title: "Variables",
                icon: "code-glyph",
                content_generator: (container_element) => this.tabVariables(container_element),
            },
        }, this.last_position.tab_name);
        // Documentation link & GitHub links
        containerEl.createEl("p").insertAdjacentHTML("beforeend", "<a href=\"" + Documentation.index + "\">Documentation</a> - " +
            "<a href=\"" + GitHub.repository + "\">SC on GitHub</a> - " +
            "<a href=\"" + GitHub.changelog + "\">SC version: " + this.plugin.getPluginVersion() + "</a>");
        // Copyright notice
        const copyright_paragraph = containerEl.createEl("p");
        copyright_paragraph.addClass("SC-small-font");
        copyright_paragraph.insertAdjacentHTML("beforeend", `
            <em>Shell commands</em> plugin Copyright &copy; 2021 - 2024 Jarkko Linnanvirta. This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See more information in the license: <a href="${GitHub.license}">GNU GPL-3.0</a>.
        `);
        // KEEP THIS AFTER CREATING ALL ELEMENTS:
        // Scroll to the position when the settings modal was last open, but do it after content generating has finished.
        // In practise, shell command previews may take some time to appear.
        this.tab_structure.contentGeneratorPromises[this.tab_structure.active_tab_id].then(() => {
            this.rememberLastPosition(containerEl);
        });
    }
    tabShellCommands(container_element) {
        return new Promise((resolveWholeContent) => {
            // Display possible environment related warnings.
            this.displayGeneralWarnings(container_element);
            // Show a search field
            this.createSearchField(container_element);
            // A <div> element for all command input fields. New command fields can be created at the bottom of this element.
            const command_fields_container = container_element.createEl("div");
            // Fields for modifying existing commands
            let shell_commands_exist = false;
            const previewPromises = [];
            for (const command_id in this.plugin.getTShellCommands()) {
                previewPromises.push(new Promise((resolveOnePreview) => {
                    createShellCommandField(this.plugin, command_fields_container, this, command_id, this.plugin.settings.show_autocomplete_menu, () => resolveOnePreview());
                }));
                shell_commands_exist = true;
            }
            // After all shell commands' previews have been generated, resolve this tab's Promise.
            Promise.allSettled(previewPromises).then(() => resolveWholeContent());
            // 'No shell commands yet' paragraph.
            const no_shell_commands_paragraph = container_element.createEl("p", { text: "No shell commands yet, click the 'New shell command' button below." });
            if (shell_commands_exist) {
                // Shell commands exist, so do not show the "No shell commands yet" text.
                no_shell_commands_paragraph.hide();
            }
            // "New command" button
            new obsidian.Setting(container_element)
                .addButton(button => button
                .setButtonText("New shell command")
                .onClick(async () => {
                createShellCommandField(this.plugin, command_fields_container, this, "new", this.plugin.settings.show_autocomplete_menu);
                no_shell_commands_paragraph.hide();
                debugLog("New empty command created.");
            }));
        });
    }
    displayGeneralWarnings(containerElement) {
        // Installation type warnings.
        if (this.plugin.settings.show_installation_warnings) {
            switch (getObsidianInstallationType()) {
                case "Flatpak": {
                    const calloutContent = new DocumentFragment();
                    calloutContent.createEl("p").innerHTML = "When Obsidian is installed using Flatpak, shell commands are executed in an isolated environment, which may cause some commands not to work. <a href=\"" + Documentation.problems.flatpakInstallation + "\">Read more</a>.";
                    createCallout(containerElement, "warning", "Flatpak installation detected - may cause execution failures", calloutContent);
                }
            }
        }
    }
    createSearchField(container_element) {
        const search_container = container_element.createDiv();
        const search_title = "Search shell commands";
        const search_setting = new obsidian.Setting(search_container)
            .setName(search_title)
            .setDesc("Looks up shell commands' aliases, commands, ids and icons.")
            .addSearch(search_component => search_component
            .onChange((search_term) => {
            let count_matches = 0;
            for (const shell_command_id in this.plugin.getTShellCommands()) {
                let matched = false;
                // Check if a search term was defined.
                if ("" == search_term) {
                    // Show all shell commands.
                    matched = true;
                }
                else {
                    // A search term is defined.
                    // Define fields where to look for the search term
                    const t_shell_command = this.plugin.getTShellCommands()[shell_command_id];
                    const search_targets = [
                        t_shell_command.getId(),
                        t_shell_command.getConfiguration().alias,
                    ];
                    search_targets.push(...Object.values(t_shell_command.getPlatformSpecificShellCommands()));
                    // Only include icon in the search if it's defined.
                    const icon = t_shell_command.getConfiguration().icon;
                    if (icon) {
                        search_targets.push(icon);
                    }
                    // Check if it's a match
                    search_targets.forEach((search_target) => {
                        if (search_target.toLocaleLowerCase().contains(search_term.toLocaleLowerCase())) {
                            matched = true;
                            debugLog("Search " + search_term + " MATCHED " + search_target);
                        }
                    });
                }
                // Show or hide the shell command.
                const shell_command_element = document.querySelector("div.SC-id-" + shell_command_id);
                if (!shell_command_element) {
                    throw new Error("Shell command setting element does not exist with selector div.SC-id-" + shell_command_id);
                }
                if (matched) {
                    shell_command_element.removeClass("SC-hide");
                    count_matches++;
                }
                else {
                    shell_command_element.addClass("SC-hide");
                }
            }
            // Display match count
            if ("" == search_term) {
                // Don't show match count.
                search_setting.setName(search_title);
            }
            else {
                // Show match count.
                switch (count_matches) {
                    case 0: {
                        search_setting.setName("No matches");
                        break;
                    }
                    case 1: {
                        search_setting.setName("1 match");
                        break;
                    }
                    default: {
                        search_setting.setName(count_matches + " matches");
                        break;
                    }
                }
            }
        }).then((search_component) => {
            // Focus on the search field.
            search_component.inputEl.addClass("SC-focus-element-on-tab-opening");
        }));
    }
    async tabEvents(container_element) {
        // A general description about events
        container_element.createEl("p", { text: "Events introduce a way to execute shell commands automatically in certain situations, e.g. when Obsidian starts. They are set up for each shell command separately, but this tab contains general options for them." });
        // Enable/disable all events
        new obsidian.Setting(container_element)
            .setName("Enable events")
            .setDesc("This is a quick way to immediately turn off all events, if you want.")
            .addToggle(toggle => toggle
            .setValue(this.plugin.settings.enable_events)
            .onChange(async (enable_events) => {
            // The toggle was clicked.
            this.plugin.settings.enable_events = enable_events;
            if (enable_events) {
                // Register events.
                this.plugin.registerSC_Events(true);
            }
            else {
                // Unregister events.
                this.plugin.unregisterSC_Events();
            }
            await this.plugin.saveSettings();
        }));
        // A list of current enable events
        container_element.createEl("p", { text: "The following gives just a quick glance over which events are enabled on which shell commands. To enable/disable events for a shell command, go to the particular shell command's settings via the 'Shell commands' tab. The list is only updated when you reopen the whole settings panel." });
        let found_enabled_event = false;
        getSC_Events(this.plugin).forEach((sc_event) => {
            const event_enabled_t_shell_commands = sc_event.getTShellCommands();
            // Has the event been enabled for any shell commands?
            if (event_enabled_t_shell_commands.length) {
                // Yes, it's enabled.
                // Show a list of shell commands
                const paragraph_element = container_element.createEl("p", { text: sc_event.static().getTitle() });
                const list_element = paragraph_element.createEl("ul");
                event_enabled_t_shell_commands.forEach((t_shell_command) => {
                    list_element.createEl("li", { text: t_shell_command.getAliasOrShellCommand() });
                });
                found_enabled_event = true;
            }
        });
        if (!found_enabled_event) {
            container_element.createEl("p", { text: "No events are enabled for any shell commands." });
        }
    }
    async tabVariables(container_element) {
        // "Preview variables in command palette" field
        new obsidian.Setting(container_element)
            .setName("Preview variables in command palette and menus")
            .setDesc("If on, variable names are substituted with their realtime values when you view your commands in the command palette and right click context menus (if used). A nice way to ensure your commands will use correct values.")
            .addToggle(checkbox => checkbox
            .setValue(this.plugin.settings.preview_variables_in_command_palette)
            .onChange(async (value) => {
            debugLog("Changing preview_variables_in_command_palette to " + value);
            this.plugin.settings.preview_variables_in_command_palette = value;
            await this.plugin.saveSettings();
        }));
        // "Show autocomplete menu" field
        new obsidian.Setting(container_element)
            .setName("Show autocomplete menu")
            .setDesc("If on, a dropdown menu shows up when you begin writing {{variable}} names, showing matching variables and their instructions. Also allows defining custom suggestions in autocomplete.yaml file - see the documentation.")
            .addToggle(checkbox => checkbox
            .setValue(this.plugin.settings.show_autocomplete_menu)
            .onChange(async (value) => {
            debugLog("Changing show_autocomplete_menu to " + value);
            this.plugin.settings.show_autocomplete_menu = value;
            this.display(); // Re-render the whole settings view to apply the change.
            await this.plugin.saveSettings();
        }))
            .addExtraButton(extra_button => extra_button
            .setIcon("help")
            .setTooltip("Documentation: Autocomplete")
            .onClick(() => {
            gotoURL(Documentation.variables.autocomplete.index);
        }));
        // Custom variables
        new obsidian.Setting(container_element)
            .setName("Custom variables")
            .setHeading() // Make the "Variables" text bold.
            .addExtraButton(extra_button => extra_button
            .setIcon("pane-layout")
            .setTooltip("Open a pane that displays all custom variables and their values.")
            .onClick(() => {
            this.plugin.createCustomVariableView();
        }))
            .addExtraButton(extra_button => extra_button
            .setIcon("help")
            .setTooltip("Documentation: Custom variables")
            .onClick(() => {
            gotoURL(Documentation.variables.customVariables);
        }));
        // General settings for CustomVariables.
        const custom_variable_container = container_element.createDiv();
        new obsidian.Setting(custom_variable_container)
            .setName("Show notifications when values of custom variables change")
            .setDesc("Exception: no notifications will be shown for changing values manually via prompts.")
            .addDropdown(dropdownComponent => dropdownComponent
            .addOptions({
            enabled: "Via URI: Notify",
            disabled: "Via URI: Don't notify",
        })
            .setValue(this.plugin.settings.custom_variables_notify_changes_via.obsidian_uri ? "enabled" : "disabled")
            .onChange(async (selection) => {
            this.plugin.settings.custom_variables_notify_changes_via.obsidian_uri = selection === "enabled";
            await this.plugin.saveSettings();
        }))
            .addDropdown(dropdownComponent => dropdownComponent
            .addOptions({
            enabled: "Via output assignment: Notify",
            disabled: "Via output assignment: Don't notify",
        })
            .setValue(this.plugin.settings.custom_variables_notify_changes_via.output_assignment ? "enabled" : "disabled")
            .onChange(async (selection) => {
            this.plugin.settings.custom_variables_notify_changes_via.output_assignment = selection === "enabled";
            await this.plugin.saveSettings();
        }));
        // Settings for each CustomVariable
        const custom_variable_model = getModel(CustomVariableModel.name);
        this.plugin.getCustomVariableInstances().forEach((custom_variable_instance) => {
            custom_variable_model.createSettingFields(custom_variable_instance, custom_variable_container);
        });
        createNewModelInstanceButton(this.plugin, CustomVariableModel.name, container_element, custom_variable_container, this.plugin.settings);
        // Built-in variable instructions
        new obsidian.Setting(container_element)
            .setName("Built-in variables")
            .setHeading() // Make the "Variables" text bold.
            .addExtraButton(extra_button => extra_button
            .setIcon("help")
            .setTooltip("Documentation: Built-in variables")
            .onClick(() => {
            gotoURL(Documentation.variables.allVariables);
        }));
        for (const variable of this.plugin.getVariables()) {
            if (!(variable instanceof CustomVariable)) {
                const variableSettingGroupElement = container_element.createDiv();
                variableSettingGroupElement.addClass("SC-setting-group");
                // Variable name and documentation link
                const variableHeadingSetting = new obsidian.Setting(variableSettingGroupElement) // Use container_element instead of variableSettingGroup.
                    .setHeading()
                    .addExtraButton(extraButton => extraButton
                    .setIcon("help")
                    .setTooltip("Documentation: " + variable.getFullName() + " variable")
                    .onClick(() => gotoURL(variable.getDocumentationLink())) // It's always a string, because the variable is not a CustomVariable.
                );
                variableHeadingSetting.nameEl.insertAdjacentHTML("afterbegin", variable.getHelpName());
                // Variable description
                const variableDescriptionSetting = new obsidian.Setting(variableSettingGroupElement)
                    .setClass("SC-full-description") // Without this, description would be shrunk to 50% of space. This setting does not have control elements, so 100% width is ok.
                ;
                variableDescriptionSetting.descEl.insertAdjacentHTML("afterbegin", variable.help_text);
                const availability_text = variable.getAvailabilityText();
                if (availability_text) {
                    variableDescriptionSetting.descEl.insertAdjacentHTML("beforeend", "<br>" + availability_text);
                }
                // Variable default value
                const defaultValueSettingTitle = "Default value for " + variable.getFullName();
                if (variable.isAlwaysAvailable()) {
                    new obsidian.Setting(variableSettingGroupElement)
                        .setName(defaultValueSettingTitle)
                        .setDesc(variable.getFullName() + " is always available, so it cannot have a default value.");
                }
                else {
                    createVariableDefaultValueField(this.plugin, variableSettingGroupElement, defaultValueSettingTitle, variable);
                }
            }
        }
        container_element.createEl("p", { text: "When you type variables into commands, a preview text appears under the command field to show how the command will look like when it gets executed with variables substituted with their real values." });
        container_element.createEl("p", { text: "Special characters in variable values are tried to be escaped (except if you use CMD as the shell in Windows). This is to improve security so that a variable won't accidentally cause bad things to happen. If you want to use a raw, unescaped value, add an exclamation mark before the variable's name, e.g. {{!title}}, but be careful, it's dangerous!" });
        container_element.createEl("p", { text: "There is no way to prevent variable parsing. If you need {{ }} characters in your command, they won't be parsed as variables as long as they do not contain any of the variable names listed above. If you need to pass e.g. {{title}} literally to your command, there is no way to do it atm, please create a discussion in GitHub." });
        container_element.createEl("p", { text: "All variables that access the current file, may cause the command preview to fail if you had no file panel active when you opened the settings window - e.g. you had focus on graph view instead of a note = no file is currently active. But this does not break anything else than the preview." });
    }
    async tabEnvironments(container_element) {
        // "Working directory" field
        const platformName = getCurrentPlatformName();
        new obsidian.Setting(container_element)
            .setName("Working directory")
            .setDesc("A directory where your commands will be run. If empty, defaults to your vault's location. Can be relative (= a folder in the vault) or absolute (= complete from " + platformName + " filesystem root). If you are using a shell that virtualizes another operating system than " + platformName + " (e.g. 'Windows Subsystem for Linux'), you should still enter a " + platformName + " formatted path. Your shell will do a conversion if needed.")
            .addText(text => text
            .setPlaceholder(getVaultAbsolutePath(this.app))
            .setValue(this.plugin.settings.working_directory)
            .onChange(async (value) => {
            debugLog("Changing working_directory to " + value);
            this.plugin.settings.working_directory = value;
            await this.plugin.saveSettings();
        }));
        // Platforms' default shells
        const shellSelectionSettings = createShellSelectionFields(this.plugin, container_element, this.plugin.settings.default_shells, true);
        // CustomShells
        new obsidian.Setting(container_element)
            .setName("Custom shells")
            .setDesc("Define e.g. WSL (Windows Subsystem for Linux), MinGW-w64 (Git Bash), or Wine here.")
            .setHeading() // Make the "Custom shells" text bold.
            .addExtraButton(extra_button => extra_button
            .setIcon("help")
            .setTooltip("Documentation: Custom shells")
            .onClick(() => {
            gotoURL(Documentation.environments.customShells.index);
        }));
        // Settings for each CustomShell
        const customShellModel = getModel(CustomShellModel.name);
        const customShellContainer = container_element.createDiv();
        this.plugin.getCustomShellInstances().forEach((customShellInstance) => {
            customShellModel.createSettingFields(customShellInstance, customShellContainer);
        });
        createNewModelInstanceButton(this.plugin, CustomShellModel.name, container_element, customShellContainer, this.plugin.settings, (customShellInstance, main_setting) => {
            // A new CustomShell is created.
            // Open settings modal.
            customShellModel.openSettingsModal(customShellInstance, main_setting).then(() => {
                // CustomShellModal is closed. Can get a shell name now.
                // Add the new shell to shell selection dropdown.
                const customShellHostPlatformId = customShellInstance.configuration.host_platform;
                shellSelectionSettings[customShellHostPlatformId].components.forEach((component) => {
                    if (component instanceof obsidian.DropdownComponent) {
                        // Add the new shell to the dropdown.
                        component.addOption(customShellInstance.getId(), customShellInstance.getTitle());
                    }
                });
            });
        });
        // PATH environment variable fields
        createPATHAugmentationFields(this.plugin, container_element, this.plugin.settings.environment_variable_path_augmentations);
    }
    async tabPreactions(container_element) {
        // Prompts
        const prompt_model = getModel(PromptModel.name);
        new obsidian.Setting(container_element)
            .setName("Prompts")
            .setHeading() // Make the "Prompts" text to appear as a heading.
        ;
        const prompts_container_element = container_element.createDiv();
        this.plugin.getPrompts().forEach((prompt) => {
            prompt_model.createSettingFields(prompt, prompts_container_element);
        });
        // 'New prompt' button
        createNewModelInstanceButton(this.plugin, PromptModel.name, container_element, prompts_container_element, this.plugin.settings, (instance, mainSetting) => {
            prompt_model.openSettingsModal(instance, mainSetting); // Open the prompt settings modal, as the user will probably want to configure it now anyway.
        });
    }
    async tabOutput(container_element) {
        // Output wrappers
        const output_wrapper_model = getModel(OutputWrapperModel.name);
        new obsidian.Setting(container_element)
            .setName("Output wrappers")
            .setHeading() // Make the "Output wrappers" text to appear as a heading.
            .addExtraButton(extra_button => extra_button
            .setIcon("help")
            .setTooltip("Documentation: Output wrappers")
            .onClick(() => gotoURL(Documentation.outputHandling.outputWrappers)));
        const output_wrappers_container_element = container_element.createDiv();
        this.plugin.getOutputWrappers().forEach((output_wrapper) => {
            output_wrapper_model.createSettingFields(output_wrapper, output_wrappers_container_element);
        });
        // 'New output wrapper' button
        createNewModelInstanceButton(this.plugin, OutputWrapperModel.name, container_element, output_wrappers_container_element, this.plugin.settings, (instance, mainSetting) => {
            output_wrapper_model.openSettingsModal(instance, mainSetting); // Open the output wrapper settings modal, as the user will probably want to configure it now anyway.
        });
        // "Error message duration" field
        this.createNotificationDurationField(container_element, "Error message duration", "Concerns messages about failed shell commands.", "error_message_duration");
        // "Notification message duration" field
        this.createNotificationDurationField(container_element, "Notification message duration", "Concerns informational, non-fatal messages, e.g. output directed to 'Notification balloon'.", "notification_message_duration");
        // "Show a notification when executing shell commands" field
        createExecutionNotificationField(container_element, this.plugin.settings.execution_notification_mode, false, // This is main settings, so don't enable a "default" option.
        this.plugin.settings.notification_message_duration, async (newExecutionNotificationMode) => {
            // Save the change.
            this.plugin.settings.execution_notification_mode = newExecutionNotificationMode;
            await this.plugin.saveSettings();
        });
        // "Outputting to 'Clipboard' displays a notification message, too" field
        new obsidian.Setting(container_element)
            .setName(obsidian.sanitizeHTMLToDom("Outputting to <em>Clipboard</em> displays a notification message, too"))
            .setDesc("If a shell command's output is directed to the clipboard, also show the output in a popup box in the top right corner. This helps to notice what was inserted into clipboard.")
            .addToggle(checkbox => checkbox
            .setValue(this.plugin.settings.output_channel_clipboard_also_outputs_to_notification)
            .onChange(async (value) => {
            this.plugin.settings.output_channel_clipboard_also_outputs_to_notification = value;
            await this.plugin.saveSettings();
        }));
        // "Outputting to 'Notification/error balloon' uses monospace formatting" field.
        const initialNotificationDecoration = this.plugin.settings.output_channel_notification_decorates_output;
        new obsidian.Setting(container_element)
            .setName(obsidian.sanitizeHTMLToDom("Outputting to <em>Notification/error balloon</em> uses monospace formatting"))
            .setDesc("Monospace formatting is achieved by wrapping output in a <code></code> element. It's good for error messages, but not optimal for long natural language texts. The formatting is only applied for messages originating from shell command execution, not for the plugin's own error messages or notifications.")
            .addDropdown(dropdownComponent => dropdownComponent
            .addOptions({
            all: "For stdout and stderr",
            stderr: "For stderr only",
            none: "Disable",
        })
            .setValue(initialNotificationDecoration === "stderr"
            ? "stderr"
            : initialNotificationDecoration ? "all" : "none")
            .onChange(async (decorationOption) => {
            switch (decorationOption) {
                case "all":
                    this.plugin.settings.output_channel_notification_decorates_output = true;
                    break;
                case "stderr":
                    this.plugin.settings.output_channel_notification_decorates_output = "stderr";
                    break;
                case "none":
                    this.plugin.settings.output_channel_notification_decorates_output = false;
                    break;
                default:
                    throw new Error("Unrecognized decorationOption: " + decorationOption);
            }
            await this.plugin.saveSettings();
        }))
            .addExtraButton(fontButton => fontButton
            .setIcon("type-outline")
            .setTooltip("Manage Obsidian's Monospace font")
            .onClick(() => {
            // Go to Appearance -> "Monospace font" setting.
            // @ts-ignore This is PRIVATE API access. Not good, but then again the feature is not crucial - if it breaks, it won't interrupt anything important.
            const appearanceTabOpened = this.plugin.app.setting?.openTabById?.("appearance");
            if (appearanceTabOpened) {
                // Try to look for the monospace font setting. This is a bit quirky and might not work if Obsidian changes the setting's name (or if user has other display language than English).
                let settingFound = false;
                document.querySelectorAll("div.setting-item-name").forEach((divSettingItemName) => {
                    if (divSettingItemName.innerHTML.match(/^\s*Monospace font\s*$/i) && !settingFound) { // !settingFound: Don't search anymore if an element was already scrolled into view.
                        // Found the monospace font setting.
                        // Ensure it's in the view and make it bold.
                        divSettingItemName.scrollIntoView({
                            behavior: "smooth",
                            block: "center",
                            inline: "nearest", // Horizontal alignment. Doesn't matter, there should be no horizontal scrolling.
                        });
                        divSettingItemName.style.fontWeight = "bold"; // Highlight the setting.
                        settingFound = true;
                    }
                });
                if (!settingFound) {
                    // No luck this time finding the setting.
                    this.plugin.newNotification("Please look for \"Monospace font\" setting.");
                }
            }
            else {
                // "Appearance" tab opening failed.
                this.plugin.newNotification("I'm not able to show the setting for you. Please look for \"Appearance\" -> \"Monospace font\".");
            }
        }));
    }
    createNotificationDurationField(container_element, title, description, setting_name) {
        new obsidian.Setting(container_element)
            .setName(title)
            .setDesc(description + " In seconds, between 1 and 180.")
            .addText(field => field
            .setValue(String(this.plugin.settings[setting_name]))
            .onChange(async (duration_string) => {
            const duration = parseInt(duration_string);
            if (duration >= 1 && duration <= 180) {
                debugLog("Change " + setting_name + " from " + this.plugin.settings[setting_name] + " to " + duration);
                this.plugin.settings[setting_name] = duration;
                await this.plugin.saveSettings();
                debugLog("Changed.");
            }
            // Don't show a notice if duration is not between 1 and 180, because this function is called every time a user types in this field, so the value might not be final.
        }));
    }
    rememberLastPosition(container_element) {
        const last_position = this.last_position;
        // Go to last position now
        this.tab_structure.buttons[last_position.tab_name].click();
        // window.setTimeout(() => { // Need to delay the scrolling a bit. Without this, something else would override scrolling and scroll back to 0.
        container_element.scrollTo({
            top: this.last_position.scroll_position,
            behavior: "auto",
        });
        // }, 0); // 'timeout' can be 0 ms, no need to wait any longer.
        // I guess there's no need for setTimeout() anymore, as rememberLastPosition() is now called after waiting for asynchronous tab content generating is finished.
        // TODO: Remove the commented code after a while.
        // Listen to changes
        container_element.addEventListener("scroll", (event) => {
            this.last_position.scroll_position = container_element.scrollTop;
        });
        for (const tab_name in this.tab_structure.buttons) {
            const button = this.tab_structure.buttons[tab_name];
            button.onClickEvent((event) => {
                last_position.tab_name = tab_name;
            });
        }
    }
}

/**
 * Copied 2021-10-29 from https://gist.github.com/TheDistantSea/8021359
 * Modifications:
 *  - Made compatible with TypeScript by adding type definitions.
 *  - Changed var to let.
 *
 * Compares two software version numbers (e.g. "1.7.1" or "1.2b").
 *
 * This function was born in http://stackoverflow.com/a/6832721.
 *
 * @param {string} v1 The first version to be compared.
 * @param {string} v2 The second version to be compared.
 * @param {object} [options] Optional flags that affect comparison behavior:
 * <ul>
 *     <li>
 *         <tt>lexicographical: true</tt> compares each part of the version strings lexicographically instead of
 *         naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than
 *         "1.2".
 *     </li>
 *     <li>
 *         <tt>zeroExtend: true</tt> changes the result if one version string has less parts than the other. In
 *         this case the shorter string will be padded with "zero" parts instead of being considered smaller.
 *     </li>
 * </ul>
 * @returns {number|NaN}
 * <ul>
 *    <li>0 if the versions are equal</li>
 *    <li>a negative integer iff v1 < v2</li>
 *    <li>a positive integer iff v1 > v2</li>
 *    <li>NaN if either version string is in the wrong format</li>
 * </ul>
 *
 * @copyright by Jon Papaioannou (["john", "papaioannou"].join(".") + "@gmail.com")
 * @license This function is in the public domain. Do what you want with it, no strings attached.
 */
function versionCompare(v1, v2, options = {}) {
    let lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend, v1parts = v1.split('.'), v2parts = v2.split('.');
    function isValidPart(x) {
        return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
    }
    if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
        return NaN;
    }
    if (zeroExtend) {
        while (v1parts.length < v2parts.length)
            v1parts.push("0");
        while (v2parts.length < v1parts.length)
            v2parts.push("0");
    }
    if (!lexicographical) {
        v1parts = v1parts.map(Number);
        v2parts = v2parts.map(Number);
    }
    for (let i = 0; i < v1parts.length; ++i) {
        if (v2parts.length == i) {
            return 1;
        }
        if (v1parts[i] == v2parts[i]) {
            continue;
        }
        else if (v1parts[i] > v2parts[i]) {
            return 1;
        }
        else {
            return -1;
        }
    }
    if (v1parts.length != v2parts.length) {
        return -1;
    }
    return 0;
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2024 Jarkko Linnanvirta
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.0 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
 */
class SC_Plugin extends obsidian.Plugin {
    constructor() {
        super(...arguments);
        this.obsidian_commands = {};
        this.t_shell_commands = {};
        /**
         * Holder for shell commands and aliases, whose variables are parsed before the actual execution during command
         * palette preview. This array gets emptied after every time a shell command is executed via the command palette.
         *
         * This is only used for command palette, not when executing a shell command from the settings panel, nor when
         * executing shell commands via SC_Events.
         *
         * @private
         */
        this.cached_parsing_processes = {};
        this.autocompleteMenus = [];
        /**
         * - Executing shell commands via menus/events does not assign the command to this property.
         * - A shell command is assigned here even if variable parsing was unsuccessful and execution cancelled.
         *
         * Used by the "Re-execute" command.
         *
         * @private
         */
        this.lastTShellCommandExecutedFromCommandPalette = null;
        /**
         * Extensions can be added to this array, but do not replace the array with a new array.
         * @private
         */
        this.editorExtensions = [];
    }
    async onload() {
        // debugLog('loading plugin'); // Wouldn't do anything, as DEBUG_ON is not set before settings are loaded.
        // Load settings
        if (!await this.loadSettings()) {
            // Loading the settings has failed due to an unsupported settings file version.
            // The plugin should not be used, and it has actually disabled itself, but the code execution needs to be
            // stopped manually.
            return;
        }
        // Now debugLog() can be used.
        debugLog("Loading Shell commands plugin version: " + this.getPluginVersion());
        // Define models
        introduceModels(this);
        // Run possible configuration migrations
        await RunMigrations(this);
        // Define builtin shells
        registerBuiltinShells(this);
        // Load CustomShells
        const customShellModel = getModel(CustomShellModel.name);
        this.customShellInstances = customShellModel.loadInstances(this.settings);
        // Generate TShellCommand objects from configuration (only after configuration migrations are done)
        this.loadTShellCommands();
        // Load Prompts
        const prompt_model = getModel(PromptModel.name);
        this.prompts = prompt_model.loadInstances(this.settings);
        // Load CustomVariables (configuration instances)
        const custom_variable_model = getModel(CustomVariableModel.name);
        this.custom_variable_instances = custom_variable_model.loadInstances(this.settings);
        // Load variables (both built-in and custom ones). Do this AFTER loading configs for custom variables!
        this.variables = loadVariables(this);
        // Load output wrappers
        const output_wrapper_model = getModel(OutputWrapperModel.name);
        this.output_wrappers = output_wrapper_model.loadInstances(this.settings);
        // Make shell commands and other commands appear in Obsidian's command palette.
        this.registerCommandPaletteCommands();
        // Perform event registrations, if enabled.
        if (this.settings.enable_events) {
            this.registerSC_Events(false);
        }
        this.registerEditorExtension(this.editorExtensions);
        // Load a custom autocomplete list if it exists.
        this.loadCustomAutocompleteList();
        // Create a SettingsTab.
        this.addSettingTab(new SC_MainSettingsTab(this.app, this));
        // Make it possible to create CustomVariableViews.
        this.registerView(CustomVariableView.ViewType, (leaf) => new CustomVariableView(this, leaf));
        this.addRibbonIcon(CustomVariableView.getIconStatic(), CustomVariableView.getDisplayTextStatic(), () => this.createCustomVariableView());
        // Debug reserved IDs
        debugLog("IDGenerator's reserved IDs:");
        debugLog(getIDGenerator().getReservedIDs());
        // Register an URI handler.
        this.registerURIHandler();
    }
    loadTShellCommands() {
        this.t_shell_commands = {}; // TODO: Consider changing this to either an array or a Map.
        const shell_command_configurations = this.getShellCommandConfigurations();
        for (const shell_command_configuration of shell_command_configurations) {
            this.t_shell_commands[shell_command_configuration.id] = new TShellCommand(this, shell_command_configuration);
        }
    }
    getTShellCommands() {
        return this.t_shell_commands;
    }
    /**
     * TODO: Change this.t_shell_commands to a Map, so that getTShellCommands() returns a Map, and remove this method.
     */
    getTShellCommandsAsMap() {
        const tShellCommandsMap = new TShellCommandMap();
        for (const shellCommandId of Object.getOwnPropertyNames(this.t_shell_commands)) {
            tShellCommandsMap.set(shellCommandId, this.t_shell_commands[shellCommandId]);
        }
        return tShellCommandsMap;
    }
    getVariables() {
        return this.variables;
    }
    getCustomVariables() {
        return new VariableSet([
            ...Array.from(this.custom_variable_instances.values()).map((customVariableInstance) => customVariableInstance.getCustomVariable()),
        ]);
    }
    getPrompts() {
        return this.prompts;
    }
    getCustomVariableInstances() {
        return this.custom_variable_instances;
    }
    getCustomShellInstances() {
        return this.customShellInstances;
    }
    getShellCommandConfigurations() {
        return this.settings.shell_commands;
    }
    getOutputWrappers() {
        return this.output_wrappers;
    }
    /**
     * Tries to find an index at which a ShellCommandConfiguration object is located in this.settings.shell_commands.
     * Returns undefined, if it's not found.
     *
     * DO NOT EXPOSE THE INDEX OUTSIDE THE PLUGIN! It's not a stable reference to a shell command, because shell commands
     * can be reordered (well, at least in some future version of the plugin). Always use the ID as a stable, externally
     * safe reference!
     *
     * @param shell_command_id
     */
    getShellCommandConfigurationIndex(shell_command_id) {
        return this.settings.shell_commands.findIndex((shell_command_configuration) => {
            return shell_command_configuration.id == shell_command_id;
        });
    }
    /**
     * Returns an Obsidian URI that complies with the format obsidian://action/?vault=XYZ and that may contain possible
     * custom arguments at the end.
     *
     * Note that if 'action' is 'open' and a 'file' argument is present in 'uri_arguments', the URI will use the shorthand syntax described here: https://help.obsidian.md/Advanced+topics/Using+obsidian+URI#Shorthand+formats
     *
     * @param action
     * @param uri_arguments
     */
    getObsidianURI(action, uri_arguments = {}) {
        const encoded_vault_name = encodeURIComponent(this.app.vault.getName());
        let base_uri;
        // Check which kind of uri type should be used: shorthand or normal
        if ("open" === action && uri_arguments.file !== undefined) {
            // Use shorthand uri type for opening a file.
            const encoded_file = encodeURIComponent(uri_arguments.file);
            base_uri = `obsidian://vault/${encoded_vault_name}/${encoded_file}`;
            delete uri_arguments.file; // Prevent adding an extra '&file=' argument to the end of the URI.
        }
        else {
            // Use normal uri type for everything else.
            base_uri = `obsidian://${action}/?vault=${encoded_vault_name}`;
        }
        let concatenated_uri_arguments = "";
        for (const uri_argument_name in uri_arguments) {
            const uri_argument_value = encodeURIComponent(uri_arguments[uri_argument_name]);
            concatenated_uri_arguments += `&${uri_argument_name}=${uri_argument_value}`;
        }
        return base_uri + concatenated_uri_arguments;
    }
    /**
     * Creates a new shell command object and registers it to Obsidian's command palette, but does not save the modified
     * configuration to disk. To save the addition, call saveSettings().
     */
    newTShellCommand() {
        const shell_command_id = getIDGenerator().generateID();
        const shell_command_configuration = newShellCommandConfiguration(shell_command_id);
        this.settings.shell_commands.push(shell_command_configuration);
        const t_shell_command = new TShellCommand(this, shell_command_configuration);
        this.t_shell_commands[shell_command_id] = t_shell_command;
        if (t_shell_command.canAddToCommandPalette()) { // This is probably always true, because the default configuration enables adding to the command palette, but check just in case.
            t_shell_command.registerToCommandPalette();
        }
        return t_shell_command;
    }
    registerCommandPaletteCommands() {
        // Shell commands.
        const shell_commands = this.getTShellCommands();
        for (const shell_command_id in shell_commands) {
            const t_shell_command = shell_commands[shell_command_id];
            if (t_shell_command.canAddToCommandPalette()) {
                t_shell_command.registerToCommandPalette();
            }
        }
        // Command: Re-execute last shell command.
        const reExecuteCommandConfiguration = this.settings.command_palette.re_execute_last_shell_command;
        if (reExecuteCommandConfiguration.enabled) {
            this.registerReExecuteCommand(reExecuteCommandConfiguration);
        }
    }
    registerReExecuteCommand(reExecuteCommandConfiguration) {
        const reExecutePrefix = reExecuteCommandConfiguration.prefix;
        const reExecuteCommand = {
            id: "re-execute-from-command-palette",
            name: reExecutePrefix + (this.lastTShellCommandExecutedFromCommandPalette?.getAliasOrShellCommand() ?? "Last shell command"),
            checkCallback: (isOpeningCommandPalette) => {
                const lastTShellCommand = this.lastTShellCommandExecutedFromCommandPalette;
                if (isOpeningCommandPalette) {
                    // The user is currently opening the command palette.
                    // Show the command only if a previously executed shell command can be found.
                    if (!lastTShellCommand) {
                        debugLog("No shell command is yet executed via command palette.");
                        return false;
                    }
                    // Do not execute the command yet, but parse variables for preview, if enabled in the settings.
                    debugLog("Getting re-execute preview for shell command #" + lastTShellCommand.getId());
                    const pluginPrefix = this.getPluginName() + ": "; // Normally Obsidian prefixes all commands with the plugin name automatically, but now that we are actually _editing_ a command in the palette (not creating a new one), Obsidian won't do the prefixing for us.
                    if (this.settings.preview_variables_in_command_palette) {
                        // Preparse variables.
                        const parsingProcess = lastTShellCommand.createParsingProcess(null); // No SC_Event is available when executing shell commands via the command palette / hotkeys.
                        parsingProcess.process().then((parsingSucceeded) => {
                            if (parsingSucceeded) {
                                // Parsing succeeded.
                                // Rename Obsidian command.
                                reExecuteCommand.name = pluginPrefix + reExecuteCommandConfiguration.prefix + (TShellCommand.getAliasOrShellCommandContentFromParsingResult(parsingProcess));
                                // Store the preparsed variables so that they will be used if this shell command gets executed.
                                this.cached_parsing_processes[lastTShellCommand.getId()] = parsingProcess;
                            }
                            else {
                                // Parsing failed, so use unparsed lastTShellCommand.getAliasOrShellCommand().
                                reExecuteCommand.name = pluginPrefix + reExecuteCommandConfiguration.prefix + lastTShellCommand.getAliasOrShellCommand();
                                this.cached_parsing_processes[lastTShellCommand.getId()] = undefined;
                            }
                        });
                    }
                    else {
                        // Parsing is disabled, so use unparsed lastTShellCommand.getAliasOrShellCommand().
                        reExecuteCommand.name = pluginPrefix + reExecuteCommandConfiguration.prefix + lastTShellCommand.getAliasOrShellCommand();
                        this.cached_parsing_processes[lastTShellCommand.getId()] = undefined;
                    }
                    // Can show in command palette.
                    return true;
                }
                else {
                    // Execute a shell command.
                    if (lastTShellCommand) {
                        // A previously executed shell command is found.
                        lastTShellCommand.executeOrShowErrors(this.cached_parsing_processes[lastTShellCommand.getId()]).then(() => {
                            // Remove obsolete preparsed variables from all shell commands, also from ones that were not executed.
                            this.cached_parsing_processes = {};
                        });
                    }
                    else {
                        // No previously executed shell command exists. (We only get here when a hotkey is pressed, as the re-execute command is not visible in Command palette, if no shell command is yet executed).
                        this.newError("No shell command has been executed yet.");
                    }
                }
            },
            repeatable: false, // TODO: Add a setting to ShellCommandConfiguration that could allow repeating shell commands via hotkeys, then add the `repeatable` property to registerShellCommand(), too. Default value for the setting would be false.
        };
        this.addCommand(reExecuteCommand);
    }
    /**
     * Goes through all events and all shell commands, and for each shell command, registers all the events that the shell
     * command as enabled in its configuration. Does not modify the configurations.
     *
     * @param called_after_changing_settings Set to: true, if this happens after changing configuration; false, if this happens during loading the plugin.
     */
    registerSC_Events(called_after_changing_settings) {
        // Make sure that Obsidian is fully loaded before allowing any events to trigger.
        this.app.workspace.onLayoutReady(() => {
            // Even after Obsidian is fully loaded, wait a while in order to prevent SC_Event_onActiveLeafChanged triggering right after start-up.
            // At least on Obsidian 0.12.19 it's not enough to delay until onLayoutReady, need to wait a bit more in order to avoid the miss-triggering.
            window.setTimeout(() => {
                // Iterate all shell commands and register possible events.
                const shell_commands = this.getTShellCommands();
                for (const shell_command_id in shell_commands) {
                    const t_shell_command = shell_commands[shell_command_id];
                    t_shell_command.registerSC_Events(called_after_changing_settings);
                }
            }, 0); // 0 means to call the callback on "the next event cycle", according to window.setTimeout() documentation. It should be a long enough delay. But if SC_Event_onActiveLeafChanged still gets triggered during start-up, this value can be raised to for example 1000 (= one second).
        });
    }
    /**
     * Goes through all events and all shell commands, and makes sure all of them are unregistered, e.g. will not trigger
     * automatically. Does not modify the configurations.
     */
    unregisterSC_Events() {
        // Iterate all events
        getSC_Events(this).forEach((sc_event) => {
            // Iterate all shell commands
            const shell_commands = this.getTShellCommands();
            for (const shell_command_id in shell_commands) {
                const t_shell_command = shell_commands[shell_command_id];
                sc_event.unregister(t_shell_command);
            }
        });
    }
    /**
     * Introduces a custom extension to CodeMirror. Follows Obsidian's instructions about updating extensions so that it
     * can be done outside SC_Plugin.onload(): https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Change+or+reconfigure+editor+extensions
     *
     * SC_CodeMirrorEvent should call this method instead of calling registerEditorExtension().
     *
     * @param extension
     */
    addEditorExtension(extension) {
        // Do NOT create a new array to this.editorExtensions.
        this.editorExtensions.push(extension);
        this.app.workspace.updateOptions();
    }
    /**
     * Defines an Obsidian protocol handler that allows receiving requests via obsidian://shell-commands URI.
     * @private
     */
    registerURIHandler() {
        this.registerObsidianProtocolHandler(SC_Plugin.SHELL_COMMANDS_URI_ACTION, async (parameters) => {
            const parameter_names = Object.getOwnPropertyNames(parameters);
            // Assign values to custom variables (also delete some unneeded entries from parameter_names)
            let custom_variable_assignments_failed = false;
            for (const parameter_index in parameter_names) {
                const parameter_name = parameter_names[parameter_index];
                // Check if the parameter name is a custom variable
                if (parameter_name.match(/^_/)) {
                    // This parameter defines a value for a custom variable
                    // Find the variable.
                    let found_custom_variable = false;
                    for (const variable of this.getVariables()) {
                        if (variable instanceof CustomVariable && variable.variable_name === parameter_name) {
                            // Found the correct variable.
                            found_custom_variable = true;
                            // Assign the given value to the custom variable.
                            await variable.setValue(parameters[parameter_name], "uri");
                        }
                    }
                    if (!found_custom_variable) {
                        this.newError("Shell commands URI: A custom variable does not exist: " + parameter_name);
                        custom_variable_assignments_failed = true;
                    }
                }
            }
            if (!custom_variable_assignments_failed) {
                // Determine action
                if (undefined !== parameters.execute) {
                    // Execute a shell command.
                    const executable_shell_command_id = parameters.execute;
                    parameter_names.remove("execute"); // Mark the parameter as handled. Prevents showing an error message for an unrecognised parameter.
                    // Find the executable shell command
                    let found_t_shell_command = false;
                    const shell_commands = this.getTShellCommands();
                    for (const shell_command_id in shell_commands) {
                        const t_shell_command = shell_commands[shell_command_id];
                        if (t_shell_command.getId() === executable_shell_command_id) {
                            // This is the correct shell command.
                            found_t_shell_command = true;
                            // Execute it.
                            const executor = new ShellCommandExecutor(this, t_shell_command, null);
                            await executor.doPreactionsAndExecuteShellCommand();
                        }
                    }
                    if (!found_t_shell_command) {
                        this.newError("Shell commands URI: A shell command id does not exist: " + executable_shell_command_id);
                    }
                }
            }
            // Raise errors for any left-over parameters, if exists.
            for (const parameter_name of parameter_names) {
                switch (parameter_name) {
                    case "": // For some reason Obsidian 0.14.5 adds an empty-named parameter if there are no ?query=parameters present.
                    case "action": // Obsidian provides this always. Don't show an error message for this.
                    case "vault": // Obsidian handles this parameter automatically. Just make sure no error message is displayed when this is present.
                        // Do nothing
                        break;
                    default:
                        if (parameter_name.match(/^_/)) ;
                        else {
                            // Throw an error for everything else.
                            this.newError("Shell commands URI: Unrecognised parameter: " + parameter_name);
                        }
                }
            }
        });
    }
    generateObsidianCommandId(shell_command_id) {
        return "shell-command-" + shell_command_id;
    }
    onunload() {
        debugLog('Unloading Shell commands plugin.');
        // Close CustomVariableViews.
        this.app.workspace.detachLeavesOfType(CustomVariableView.ViewType);
        // Close autocomplete menus.
        for (const autocompleteMenu of this.autocompleteMenus) {
            autocompleteMenu?.destroy();
        }
    }
    /**
     *
     * @param current_settings_version
     * @private
     * @return True if the given settings version is supported by this plugin version, or an error message string if it's not supported.
     */
    isSettingsVersionSupported(current_settings_version) {
        if (current_settings_version === "prior-to-0.7.0") {
            // 0.x.y supports all old settings formats that do not define a version number. This support will be removed in 1.0.0.
            return true;
        }
        else {
            // Compare the version number
            /** Note that the plugin version may be different than what will be used in the version comparison. The plugin version will be displayed in possible error messages. */
            const plugin_version = this.getPluginVersion();
            const version_comparison = versionCompare(SC_Plugin.SettingsVersion, current_settings_version);
            if (version_comparison === 0) {
                // The versions are equal.
                // Supported.
                return true;
            }
            else if (version_comparison < 0) {
                // The compared version is newer than what the plugin can support.
                return "The settings file is saved by a newer version of this plugin, so this plugin does not support the structure of the settings file. Please upgrade this plugin to at least version " + current_settings_version + ". Now the plugin version is " + plugin_version;
            }
            else {
                // The compared version is older than the version that the plugin currently uses to write settings.
                // 0.x.y supports all old settings versions. In 1.0.0, some old settings formats might lose their support, but that's not yet certain.
                return true;
            }
        }
    }
    getPluginVersion() {
        return this.manifest.version;
    }
    async loadSettings() {
        // Try to read a settings file
        let all_settings;
        this.settings = await this.loadData(); // May have missing main settings fields, if the settings file is from an older version of SC. It will be migrated later.
        if (null === this.settings) {
            // The settings file does not exist.
            // Use default settings
            this.settings = getDefaultSettings(true);
            all_settings = this.settings;
        }
        else {
            // Succeeded to load a settings file.
            // In case the settings file does not have 'debug' or 'settings_version' fields, create them.
            all_settings = combineObjects(getDefaultSettings(false), this.settings); // This temporary settings object always has all fields defined (except sub fields, such as shell command specific fields, may still be missing, but they are not needed this early). This is used so that it's certain that the fields 'debug' and 'settings_version' exist.
        }
        // Update debug status - before this line debugging is always OFF!
        setDEBUG_ON(all_settings.debug);
        // Ensure that the loaded settings file is supported.
        const version_support = this.isSettingsVersionSupported(all_settings.settings_version);
        if (typeof version_support === "string") {
            // The settings version is not supported.
            new obsidian.Notice("SHELL COMMANDS PLUGIN HAS DISABLED ITSELF in order to prevent misinterpreting settings / corrupting the settings file!", 120 * 1000);
            new obsidian.Notice(version_support, 120 * 1000);
            await this.disablePlugin();
            return false; // The plugin should not be used.
        }
        return true; // Settings are loaded and the plugin can be used.
    }
    async saveSettings() {
        // Update settings version in case it's old.
        this.settings.settings_version = SC_Plugin.SettingsVersion;
        // Write settings
        await this.saveData(this.settings);
        // Trigger an event so that any program parts relying on cached configuration will know to flush their caches.
        document.dispatchEvent(new Event("SC-configuration-change"));
    }
    loadCustomAutocompleteList() {
        const custom_autocomplete_file_name = "autocomplete.yaml";
        const custom_autocomplete_file_path = path__namespace.join(getPluginAbsolutePath(this, isWindows()), custom_autocomplete_file_name);
        if (fs__namespace.existsSync(custom_autocomplete_file_path)) {
            debugLog("loadCustomAutocompleteList(): " + custom_autocomplete_file_name + " exists, will load it now.");
            const custom_autocomplete_content = fs__namespace.readFileSync(custom_autocomplete_file_path).toLocaleString();
            const result = addCustomAutocompleteItems(custom_autocomplete_content);
            if (true === result) {
                // OK
                debugLog("loadCustomAutocompleteList(): " + custom_autocomplete_file_name + " loaded.");
            }
            else {
                // An error has occurred.
                debugLog("loadCustomAutocompleteList(): " + result);
                this.newError("Shell commands: Unable to parse " + custom_autocomplete_file_name + ": " + result);
            }
        }
        else {
            debugLog("loadCustomAutocompleteList(): " + custom_autocomplete_file_name + " does not exists, so won't load it. This is perfectly ok.");
        }
    }
    /**
     * Puts the given Autocomplete menu into a list of menus that will be destroyed when the plugin unloads.
     * @param autocompleteMenu
     */
    registerAutocompleteMenu(autocompleteMenu) {
        this.autocompleteMenus.push(autocompleteMenu);
    }
    async disablePlugin() {
        // This unfortunately accesses a private API.
        // @ts-ignore PRIVATEAPI
        await this.app.plugins.disablePlugin(this.manifest.id);
    }
    getPluginId() {
        return this.manifest.id;
    }
    getPluginName() {
        return this.manifest.name;
    }
    newError(message, timeout = this.getErrorMessageDurationMs()) {
        return new obsidian.Notice(message, timeout);
    }
    newErrors(messages) {
        messages.forEach((message) => {
            this.newError(message);
        });
    }
    /**
     *
     * @param message
     * @param timeout Custom timeout in milliseconds. If not set, the timeout will be fetched from user configurable settings. Use 0 if you want to disable the timeout, i.e. show the notification until it's explicitly hidden by clinking it, or via code.
     */
    newNotification(message, timeout = this.getNotificationMessageDurationMs()) {
        return new obsidian.Notice(message, timeout);
    }
    getNotificationMessageDurationMs() {
        return this.settings.notification_message_duration * 1000; // * 1000 = convert seconds to milliseconds.
    }
    getErrorMessageDurationMs() {
        return this.settings.error_message_duration * 1000; // * 1000 = convert seconds to milliseconds.
    }
    /**
     * Returns an explicitly selected shell identifier, if available, or a system's default shell identifier.
     */
    getDefaultShellIdentifier() {
        const platformId = getOperatingSystem();
        let shellIdentifier = this.getDefaultShellIdentifierForPlatform(platformId); // Can also be undefined.
        if (undefined === shellIdentifier) {
            shellIdentifier = getUsersDefaultShellIdentifier();
        }
        return shellIdentifier;
    }
    /**
     * Returns undefined, if a system's default shell is used for the given operating system.
     *
     * @param platformId
     */
    getDefaultShellIdentifierForPlatform(platformId) {
        const shellIdentifier = this.settings.default_shells[platformId];
        if (undefined === shellIdentifier && platformId === getOperatingSystem()) {
            // Operating system's default shell should be used, and the passed operating system is the currently running
            // operating system, so it's possible to determine the actual shell.
            return getUsersDefaultShellIdentifier();
        }
        // Note that shellIdentifier can still be undefined.
        return shellIdentifier;
    }
    /**
     * Returns an explicitly selected shell, if available, or a system's default shell identifier.
     */
    getDefaultShell() {
        return getShell(this, this.getDefaultShellIdentifier());
    }
    /**
     * Returns null, if a system's default shell is used for the given operating system.
     *
     * @param platformId
     */
    getDefaultShellForPlatform(platformId) {
        const shellIdentifier = this.getDefaultShellIdentifierForPlatform(platformId);
        if (undefined === shellIdentifier) {
            return null;
        }
        else {
            return getShell(this, shellIdentifier);
        }
    }
    createCustomVariableView() {
        const leaf = this.app.workspace.getRightLeaf(false);
        leaf.setViewState({
            type: CustomVariableView.ViewType,
            active: true,
        }).then();
        this.app.workspace.revealLeaf(leaf);
    }
    /**
     * Called when CustomVariable values are changed.
     */
    async updateCustomVariableViews() {
        for (const leaf of this.app.workspace.getLeavesOfType(CustomVariableView.ViewType)) {
            if (leaf && leaf.view instanceof CustomVariableView) {
                await leaf.view.updateContent();
            }
            // If leaf.view is not CustomVariableView, it's probably DeferredView, i.e. a custom variable view which has not yet been shown/opened up by user. See more: https://docs.obsidian.md/Plugins/Guides/Understanding+deferred+views#Accessing+%60leaf.view%60
        }
    }
    /**
     * Used by OutputChannel_StatusBar.
     * TODO: Make it possible to have multiple status bar elements. It should be a shell command level setting, where a shell command opts for either to use their own status bar element, or a common one.
     */
    getOutputStatusBarElement() {
        if (!this.statusBarElement) {
            this.statusBarElement = this.addStatusBarItem();
        }
        return this.statusBarElement;
    }
    /**
     * Creates an icon button that when clicked, will send a request to terminate shell command execution intermittently.
     *
     * @param containerElement
     * @param processTerminator A callback that will actually terminate the shell command execution process.
     */
    createRequestTerminatingButton(containerElement, processTerminator) {
        const button = containerElement.createEl('a', {
            prepend: true,
            attr: {
                "aria-label": "Request to terminate the process",
                class: "SC-icon-terminate-process",
            },
        });
        obsidian.setIcon(button, "power");
        button.onclick = (event) => {
            processTerminator();
            event.preventDefault();
            event.stopPropagation();
        };
    }
}
/**
 * Defines the settings structure version. Change this when a new plugin version is released, but only if that plugin
 * version introduces changes to the settings structure. Do not change if the settings structure stays unchanged.
 *
 * This is NOT the plugin's version (although they are often coupled the same)! The plugin's version is defined in manifest.json.
 */
SC_Plugin.SettingsVersion = "0.23.0";
SC_Plugin.SHELL_COMMANDS_URI_ACTION = "shell-commands";

module.exports = SC_Plugin;


/* nosourcemap */