Resolve user environment in local links before opening

Currently, at least on Windows, if you try to open a link like:

[%LocalAppData%/nvim](<file:///%LocalAppData%/nvim>)  

you will get an error because %LocalAppData% is processed as it is.

I wrote a script that detects when user clicks on links containing a user environment and resolves it before opening the path.

Currently, it handles Windows only. You are welcome if you want to post your solution to handle other OS.

List of user environments that can be resolved:

Prerequisite

It is not a plugin but a JS script. I personally use user plugins to run my scripts. You could use another solution like CodeScript Toolkit but I didn’t test my script with it. It could require some modification to work with it.

Setup

Create a file named like Resolve Env in link.js and put it in your the scripts folder set in user plugins. In my vault, it is MyScripts/UserPlugins. Paste this code in the file:

// check https://forum.obsidian.md/t/resolve-user-environment-in-local-links-before-opening/106566
module.exports = {};
const Plugin = require("obsidian").Plugin;
const { exec } = require("child_process");
const { shell } = require("electron");

const prefix = "Resolve Env in link";

module.exports.default = class MyPlugin extends Plugin {
	documentClickHandler = (evt) => {
		const target = evt.target;
		// if target is not a url, exit
		if (!(target instanceof HTMLAnchorElement)) {
			return;
		}

		const href = target.href;
		log(`User clicked on the link: "${href}"`);

		if (!href.startsWith("file:///")) {
			log("Ignored because link is not a local path.");
			return;
		}

		let transformedHref = decodeURIComponent(href);
		log(`Decoded link: "${transformedHref}"`);

		let path = transformedHref.replace("file:///", "");
		log(`Trying to resolve this path: "${path}"`);

		log(`path match /%\\w+%/ : ${/%\w+%/.test(path)}`);

		// Apply resolveEnv only if %env% is present
		if (/%\w+%/.test(path)) {
			path = resolveEnv(path);
			log(`Resolved path: "${path}"`);
		} else {
			log("No env var pattern found, keeping original path.");
		}

		transformedHref = `file:///${encodeURIComponent(path)}`;

		log(`Open this new url: "${transformedHref}"`);

		evt.preventDefault();
		evt.stopPropagation();
		evt.stopImmediatePropagation();

		try {
			// Using exec to run a shell command is required to open the default app in foreground (at least on Windows)
			const command = getOpenCommand(path);
			log(`Executing command: ${command}`);

			new Promise((resolve, reject) => {
				exec(command, (error, stdout, stderr) => {
					if (error) {
						log(`Error opening file: ${error.message}`);
						reject(error);
					} else {
						log("File opened successfully.");
						resolve(null);
					}
				});
			});
		} catch (error) {
			log(`Error opening file: ${error.message}`);
		}
	};

	async onload() {
		log("script loaded. Listening click events.");

		document.addEventListener("click", this.documentClickHandler, true);
	}

	async onunload() {
		log("script unloaded. Click events unregistered.");

		document.removeEventListener("click", this.documentClickHandler, true);
	}
};

/**==========================================================================
* ℹ		UTILS
===========================================================================*/
function resolveEnv(path) {
	log("START resolveEnv");

	// Create oEnv object with specified keys
	const oEnv = {
		AppData: process.env.APPDATA,
		LocalAppData: process.env.LOCALAPPDATA,
		OneDrive: process.env.ONEDRIVE,
		UserProfile: process.env.USERPROFILE,
		AllUsersProfile: process.env.ALLUSERSPROFILE,
		ProgramData: process.env.PROGRAMDATA,
		ProgramFiles: process.env.PROGRAMFILES,
		WinDir: process.env.WINDIR,
	};

	// Debug object
	log(`oEnv: ${JSON.stringify(oEnv, null, 2)}`);

	// Check if the path begins with one of the oEnv key
	for (const [key, value] of Object.entries(oEnv)) {
		if (value && path.startsWith(`%${key}%`)) {
			// Replace the corresponding part with the environment variable
			const newPath = path.replace(`%${key}%`, value);
			log(`Path matched with ${key}, newPath: ${newPath}`);
			return newPath;
		}
	}

	// If no match, return the original path
	log("No environment variable match found, returning original path");
	return path;
}

function getOpenCommand(path) {
	const platform = process.platform;
	if (platform === "win32") {
		return `start "" "${path.replace(/"/g, '\\"')}"`;
	} else if (platform === "darwin") {
		return `open "${path}"`;
	} else {
		return `xdg-open "${path}"`; // Linux
	}
}

function log(msg) {
	console.log(`[${prefix}] ${msg}`);
}

In user plugins settings, reload the scripts then enable Resolve Env in link.js

Test

Copy a path containing a user environment. You can use this templater script to auto convert your local path to a string using the right user environment and pretty paste the link in your note.

Open the link. It should open with no error.

Update

Subscribe to this topic to be notified of updates.

Dev

You’re welcome to transform this script into a plugin, improve it, etc. Please just share with us in this topic.

:heart: Like if you appreciated this tip.