Last active
July 28, 2026 21:38
-
-
Save jakecraige/f03d32bf1df1b3636e3745147ac9d811 to your computer and use it in GitHub Desktop.
A (partial) replacement for obsidian-shellcommands for direct use with Obsidian QuickAdd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * This script is intended to be used with QuickAdd to support spawning custom subprocesses | |
| * which have context about the current note being viewed in Obsidian. | |
| * | |
| * It functions as a much more incomplete version of https://community.obsidian.md/plugins/obsidian-shellcommands | |
| * But is sufficient for my needs and helps me avoid extra plugin dependencies. | |
| * | |
| * Usage: | |
| * 1. Add this file as a User Script for QuickAdd (https://quickadd.obsidian.guide/docs/Choices/MacroChoice/#add-a-user-script-command) | |
| * 2. Create a "Macro Choice" in QuickAdd | |
| * 3. In its options, select this user script to add it as a step in the macro. | |
| * 4. Use the settings to configure the command to be executed. | |
| * - Uses QuickAdd's Format Syntax: https://quickadd.obsidian.guide/docs/FormatSyntax/ | |
| * - Tip: If you want user input (or much more) for the commands, see how to do so in their docs. | |
| * - Custom variables provided for use in command template: | |
| * workingDir (absolute path), vaultDir (absolute path), | |
| * fileName, fileExtension, | |
| * filePath (relative to working directory), fileDir (relative to working directory) | |
| * 5. That's all! Active it from QuickAdd's. I've also included the commandOutput in the variables so this could be chained with | |
| * other scripts if desired, though it's not a feature I've used yet. | |
| * | |
| * SECURITY NOTICE: Obviously running arbitrary shell commands with dynamic input can be dangerous and allow for fairly arbitrary | |
| * access to your device. Be careful about what inputs you use to this. If they can be controlled by third-parties, that exposes | |
| * you to serious risk. | |
| * | |
| * Cheers, Jake Craige (jakecraige) | |
| * License: MIT | |
| **/ | |
| const notice = (msg) => new Notice(msg, 5000); | |
| const log = (msg) => console.log('shell-command:', msg); | |
| module.exports = { | |
| entry: start, | |
| settings: { | |
| name: "Shell Commands", | |
| author: "Jake Craige", | |
| options: { | |
| "Command Template": { | |
| type: "format", | |
| placeholder: "echo {{VALUE:filePath}}", | |
| description: "Command to execute within your default shell. Available variables: workingDir, vaultDir, fileName, fileExtension, filePath, fileDir" | |
| }, | |
| "Show Output": { | |
| type: "toggle", | |
| defaultValue: true, | |
| description: "Show command output in a popup when complete" | |
| }, | |
| "Working Directory For Execution": { | |
| type: "dropdown", | |
| defaultValue: "active-note-directory", | |
| options: ["active-note-directory", "vault-root"], | |
| description: "Which directory will the command be run from" | |
| }, | |
| "Execution Timeout (ms)": { | |
| type: "number", | |
| defaultValue: 10000, | |
| description: "Maximum time for the process to execute before being killed" | |
| }, | |
| } | |
| } | |
| } | |
| async function start(params, settings) { | |
| const { app, quickAddApi, obsidian, variables } = params; | |
| const executionTimeout = parseInt(settings["Execution Timeout (ms)"], 10); | |
| const showOutput = settings["Show Output"]; | |
| const workingDirSelection = settings["Working Directory For Exectution"]; | |
| const commandFormat = settings["Command Template"]; | |
| if (commandFormat === "") { | |
| notice("No command template provided, set one in the settings"); | |
| return; | |
| } | |
| const vaultAbsolutePath = getVaultAbsolutePath(app, obsidian); | |
| const activeFile = app.workspace.getActiveFile(); | |
| const path = require("path"); | |
| let workingDir = null; | |
| switch (workingDirSelection) { | |
| case "active-note-directory": | |
| if (!activeFile) { | |
| notice("No active file -- cannot use active-note-directory working directory option."); | |
| return; | |
| } | |
| workingDir = path.join(vaultAbsolutePath, activeFile.parent.path); | |
| break; | |
| case "vault-root": | |
| workingDir = vaultAbsolutePath; | |
| break; | |
| default: | |
| throw new Error("Invalid working directory dropdown selection: " + workingDirSelection); | |
| } | |
| variables.workingDir = workingDir; | |
| variables.vaultDir = vaultAbsolutePath; | |
| if (activeFile) { | |
| variables.fileName = activeFile.basename; | |
| variables.fileExtension = activeFile.extension; | |
| variables.filePath = path.relative(variables.workingDir, path.join(vaultAbsolutePath, path.normalize(activeFile.path))); | |
| variables.fileDir = path.relative(variables.workingDir, path.join(vaultAbsolutePath, path.normalize(activeFile.parent?.path))) || "."; | |
| } | |
| log("variables:") | |
| log(" workingDir: " + variables.workingDir); | |
| log(" vaultDir: " + variables.vaultDir); | |
| log(" fileName: " + variables.fileName); | |
| log(" filePath: " + variables.filePath); | |
| log(" fileDir: " + variables.fileDir); | |
| const formattedCommand = await quickAddApi.format(commandFormat); // variables automatically available to format func | |
| try { | |
| variables.commandOutput = null; | |
| variables.commandError = null; | |
| let output = await spawn(formattedCommand, { cwd: workingDir, timeout: executionTimeout, shell: true }); | |
| log("Command success: " + output); | |
| notice("Command successful."); | |
| variables.commandOutput = output; | |
| if (showOutput) { | |
| notice("Output: " + output); | |
| } | |
| } catch (err) { | |
| variables.commandError = err; | |
| log("Command failed.") | |
| log(err); | |
| notice(err); | |
| notice("Command failed."); | |
| } | |
| } | |
| function getVaultAbsolutePath(app, obsidian) { | |
| // Borrowed from here: https://github.com/Taitava/obsidian-shellcommands/blob/main/src/Common.ts#L48-L56 | |
| 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 spawn(command, args) { | |
| const childProcess = require("child_process"); | |
| return new Promise(function(resolve, reject) { | |
| log("Running command:") | |
| log(" command: " + command); | |
| log(" args: " + JSON.stringify(args)); | |
| try { | |
| let child = childProcess.spawn(command, args); | |
| let scriptOutput = ""; | |
| child.stdout.setEncoding('utf8'); | |
| child.stdout.on('data', function(data) { | |
| data=data.toString(); | |
| scriptOutput+=data; | |
| }); | |
| child.stderr.setEncoding('utf8'); | |
| child.stderr.on('data', function(data) { | |
| data=data.toString(); | |
| scriptOutput+=data; | |
| }); | |
| child.on('close', function(code) { | |
| if (code === 0) { | |
| resolve(scriptOutput); | |
| } else { | |
| console.log("reject error"); | |
| reject({code, out: scriptOutput}); | |
| } | |
| }); | |
| } catch (err) { | |
| console.log("weird error"); | |
| reject({code: null, err}); | |
| } | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment