/* 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 - 2023 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 . 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 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 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 - 2023 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 . * * 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); } /** * 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); } } /** * 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
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
if needed. if (content_line_index < content_lines.length - 1) { content_element.insertAdjacentHTML("beforeend", "
"); } }); 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
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 "•".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; } } } /** * 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 - 2023 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 . * * 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/", // Keep the trailing slash! }, outputHandling: { outputHandlingMode: "https://publish.obsidian.md/shellcommands/Output+handling/Realtime+output+handling", outputWrappers: "https://publish.obsidian.md/shellcommands/Output+handling/Output+wrappers", }, 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 - 2023 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 . * * 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 "" + this.getFullName() + ""; } /** * 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 and 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 - 2023 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 . * * 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 "Only available in output wrappers, cannot be used as input for shell commands."; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "Only available when a note pane is open, not in graph view, nor when viewing non-text files."; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "{{caret_position}}, {{caret_position:line}} or {{caret_position:column}}"; } 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 - 2023 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 . * * 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 - 2023 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 . * * 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; } /** * @param app * @param file * @param property_path * @return string|string[] Either a result string, or an array of error messages. */ function getFileYAMLValue(app, file, property_path) { const error_messages = []; const property_parts = property_path.split("."); // 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 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 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 string|string[] Either a result string, or an array of error messages. */ 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 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 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. // Freak out. const nested_elements_keys = Object.getOwnPropertyNames(property_value); 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."); } else { error_messages.push("YAML property '" + property_name + "' contains a nested element. Use a property name that points to a literal value instead."); } return 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 error_messages; } else { return property_value.toString(); } } } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "Only available when the active pane contains a file, not in graph view or other non-file view."; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "{{file_extension:with-dot}} or {{file_extension:no-dot}}"; } } Variable_FileExtension.parameters = { "dot": { options: ["with-dot", "no-dot"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{file_path:relative}} or {{file_path:absolute}}"; } } Variable_FilePath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{folder_path:relative}} or {{folder_path:absolute}}"; } } Variable_FolderPath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "Only available when something is selected in Editing/Live preview mode, not in Reading mode."; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 "Only available when the Workspaces core plugin is enabled."; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "Only available in debug mode."; } } Variable_Passthrough.parameters = { value: { type: "string", required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 result = getFileYAMLValue(this.app, this.getFileOrThrow(), castedArguments.property_name); if (Array.isArray(result)) { // The result contains error message(s). this.throw(result.join(" ")); } else { // The result is ok, it's a string. return result; } } getAvailabilityText() { return super.getAvailabilityText() + " Also, the given YAML property must exist in the file's frontmatter."; } getHelpName() { return "{{yaml_value:property}}"; } } Variable_YAMLValue.parameters = { property_name: { type: "string", required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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) { return this.supported_sc_events.contains(sc_event_class); } getSummaryOfSupportedEvents() { 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 "Only available in events: " + this.getSummaryOfSupportedEvents() + "."; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 t_shell_command * @param parsing_process SC_MenuEvent can use this to pass an already started ParsingProcess instance. If omitted, a new ParsingProcess will be created. */ async trigger(t_shell_command, parsing_process) { debugLog(this.constructor.name + ": Event triggers executing shell command id " + t_shell_command.getId()); // Execute the shell command. const executor = new ShellCommandExecutor(this.plugin, t_shell_command, this); await executor.doPreactionsAndExecuteShellCommand(parsing_process); } 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; } 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); } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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); } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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."); } }); } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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); } }; } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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; } } } /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FileCreated.event_code = "file-created"; SC_Event_FileCreated.event_title = "File created"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FileContentModified.event_code = "file-content-modified"; SC_Event_FileContentModified.event_title = "File content modified"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FileDeleted.event_code = "file-deleted"; SC_Event_FileDeleted.event_title = "File deleted"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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"; } } SC_Event_FileRenamed.event_code = "file-renamed"; SC_Event_FileRenamed.event_title = "File renamed"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FileMoved.event_code = "file-moved"; SC_Event_FileMoved.event_title = "File moved"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{event_file_path:relative}} or {{event_file_path:absolute}}"; } } Variable_EventFilePath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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"; } } SC_Event_FolderCreated.event_code = "folder-created"; SC_Event_FolderCreated.event_title = "Folder created"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FolderDeleted.event_code = "folder-deleted"; SC_Event_FolderDeleted.event_title = "Folder deleted"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FolderRenamed.event_code = "folder-renamed"; SC_Event_FolderRenamed.event_title = "Folder renamed"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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"; } } SC_Event_FolderMoved.event_code = "folder-moved"; SC_Event_FolderMoved.event_title = "Folder moved"; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{event_folder_path:relative}} or {{event_folder_path:absolute}}"; } } Variable_EventFolderPath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{event_file_extension:with-dot}} or {{event_file_extension:no-dot}}"; } } Variable_EventFileExtension.parameters = { "dot": { options: ["with-dot", "no-dot"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 result = getFileYAMLValue(this.app, sc_event.getFile(), castedArguments.property_name); if (Array.isArray(result)) { // The result contains error message(s). this.throw(result.join(" ")); } else { // The result is ok, it's a string. return result; } } getAvailabilityText() { return super.getAvailabilityText() + " Also, the given YAML property must exist in the file's frontmatter."; } getHelpName() { return "{{event_yaml_value:property}}"; } } Variable_EventYAMLValue.parameters = { property_name: { type: "string", required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "{{environment:variable}}"; } getAvailabilityText() { return "Only available if the passed environment variable name exists."; } } Variable_Environment.parameters = { variable: { type: "string", required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{event_file_path:relative}} or {{event_file_path:absolute}}"; } } Variable_EventOldFilePath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{event_file_path:relative}} or {{event_file_path:absolute}}"; } } Variable_EventOldFolderPath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 "{{folder_path:relative}} or {{folder_path:absolute}}"; } } Variable_NewNoteFolderPath.parameters = { mode: { options: ["absolute", "relative"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 "Only available in debug mode."; } } Variable_Newline.parameters = { count: { type: "integer", required: false, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "{{yaml_content:with-dashes}} or {{yaml_content:no-dashes}}"; } } Variable_YAMLContent.parameters = { withDashes: { options: ["with-dashes", "no-dashes"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "{{event_yaml_content:with-dashes}} or {{event_yaml_content:no-dashes}}"; } } Variable_EventYAMLContent.parameters = { withDashes: { options: ["with-dashes", "no-dashes"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * 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 "{{operating_system:id}}, {{operating_system:name}}, {{operating_system:release}} or {{file_path:version}}"; } getAvailabilityText() { return "Only available in debug mode."; } } Variable_OperatingSystem.parameters = { property: { options: ["id", "name"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/ */ class Variable_ObsidianAPI extends Variable { constructor() { super(...arguments); this.variable_name = "obsidian_api"; this.help_text = "Gives Obsidian's API version."; } async generateValue(shell, castedArguments) { switch (castedArguments.property) { case "version": return obsidian.apiVersion; } } getAutocompleteItems() { const autocompleteItems = [ { value: this.getFullName(false, "version"), help_text: "Gives Obsidian's API version, which follows the release cycle of the desktop application." + this.getAvailabilityText(), group: "Variables", type: "normal-variable", documentationLink: this.getDocumentationLink(), }, ]; Variable.supplementAutocompleteItems(autocompleteItems); return autocompleteItems; } getHelpName() { return "{{obsidian_api:version}}"; } getAvailabilityText() { return "Only available in debug mode."; } } Variable_ObsidianAPI.parameters = { property: { options: ["version"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/ */ class Variable_ShellCommandsPlugin extends Variable { constructor() { super(...arguments); this.variable_name = "shell_commands_plugin"; this.help_text = "Gives the Shell commands plugin's version information."; } async generateValue(shell, castedArguments) { switch (castedArguments.property) { case "plugin-version": return this.plugin.getPluginVersion(); case "settings-version": return this.plugin.settings.settings_version; } } getAutocompleteItems() { const autocompleteItems = [ { value: this.getFullName(false, "plugin-version"), help_text: "Gives the Shell commands plugin's current version. " + this.getAvailabilityText(), group: "Variables", type: "normal-variable", documentationLink: this.getDocumentationLink(), }, { value: this.getFullName(false, "settings-version"), help_text: "Gives the Shell commands plugin's settings structure version, which is almost identical to the plugin's version, but the patch version is usually 0 (in major.minor.patch version format). " + this.getAvailabilityText(), group: "Variables", type: "normal-variable", documentationLink: this.getDocumentationLink(), }, ]; Variable.supplementAutocompleteItems(autocompleteItems); return autocompleteItems; } getHelpName() { return "{{shell_commands_plugin:plugin-version}} or {{shell_commands_plugin:settings-version}}"; } getAvailabilityText() { return "Only available in debug mode."; } } Variable_ShellCommandsPlugin.parameters = { property: { options: ["plugin-version", "settings-version"], required: true, }, }; /* * 'Shell commands' plugin for Obsidian. * Copyright (C) 2021 - 2023 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 . * * Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/ */ 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), // Variable_Output is not loaded here, because it's only used in OutputWrappers. new Variable_Selection(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), // 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_EventYAMLContent(plugin), new Variable_EventYAMLValue(plugin), ]; if (DEBUG_ON) { // Variables that are only designed for 'Shell commands test suite'. built_in_variables.push(new Variable_Newline(plugin), new Variable_ObsidianAPI(plugin), new Variable_OperatingSystem(plugin), new Variable_Passthrough(plugin), new Variable_ShellCommandsPlugin(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 - 2023 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 . * * 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 "&"; if (str === "<") return "<"; if (str === ">") return ">"; if (str === "\"") return """; if (str === "'") return "'"; }); }; 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 "" + txt + ""; }; 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 = "" + this.escape_txt_for_html(pkt.text) + ""; 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 - 2023 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 . * * 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; if (combineOutputStreams) { // 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, }; } } 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 - 2023 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 . * * 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 `` element and creates a DocumentFragment for it. * @param outputStreamName Used to determine whether output can be wrapped in . * @param outputContent * @private */ prepareHTML(outputStreamName, outputContent) { // Can output be wrapped in 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 ? "" + outputContent + "" // Use instead of
 to allow line wrapping.
            : outputContent);
    }
}

/*
 * 'Shell commands' plugin for Obsidian.
 * Copyright (C) 2021 - 2023 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 .
 *
 * 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 - 2023 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 .
 *
 * 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 - 2023 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 .
 *
 * 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 - 2023 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 .
 *
 * 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|
)/u); //
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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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 - 2023 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 . * * 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