-
-
Save laundmo/de1f386fac9f9e797fd77022d63967c9 to your computer and use it in GitHub Desktop.
| /* License: MIT https://opensource.org/licenses/MIT | |
| * | |
| * Usage: Any file paths, except those in code blocks, will become links. | |
| * File paths with spaces can work by first creating a link without a space, | |
| * and then adding the parts after the space. | |
| */ | |
| function normalizePathToUrl(path) { | |
| if (/^[A-Z]:/i.test(path)) { | |
| return 'file:///' + path.replace(/\\/g, '/'); | |
| } | |
| return 'file://' + path; | |
| } | |
| function isInCodeBlock(item) { | |
| let element = item.parent; | |
| while (element) { | |
| if (element.is('element', 'codeBlock')) { | |
| return true; | |
| } | |
| element = element.parent; | |
| } | |
| if (item.hasAttribute && item.hasAttribute('code')) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| class PathLinkerWidget extends api.NoteContextAwareWidget { | |
| constructor(...args) { | |
| super(...args); | |
| this.register | |
| } | |
| get position() { | |
| // higher value means position towards the bottom/right | |
| return 100; | |
| } | |
| get parentWidget() { return 'center-pane'; } | |
| async refreshWithNote(note) { | |
| api.getActiveContextTextEditor().then(editor => { | |
| if (!editor) { | |
| return | |
| } | |
| const path_re = /(?<=^|\s)([A-Z]:[\\/]{1,2}[^\s]+|\/[^\s]+)/g; | |
| const path_start_re = /^([A-Z]:[\\/]{1,2}|\/)/; | |
| editor.model.change(writer => { | |
| const root = editor.model.document.getRoot(); | |
| const linksToUpdate = []; | |
| const range = editor.model.createRangeIn(root); | |
| for (const { item, previousPosition } of range.getWalker({ ignoreElementEnd: true })) { | |
| if (item.is('$textProxy')) { | |
| if (isInCodeBlock(item)) { | |
| continue; | |
| } | |
| const text = item.data; | |
| if (item.hasAttribute('linkHref')) { | |
| if (path_start_re.test(text)) { | |
| const startPos = previousPosition; | |
| const endPos = startPos.getShiftedBy(text.length); | |
| linksToUpdate.push({ | |
| range: writer.createRange(startPos, endPos), | |
| url: normalizePathToUrl(text) | |
| }); | |
| } | |
| } else { | |
| let match; | |
| path_re.lastIndex = 0; | |
| while ((match = path_re.exec(text)) !== null) { | |
| const pathText = match[1]; | |
| const pathStartIndex = match.index + (match[0].length - pathText.length); | |
| const startPos = previousPosition.getShiftedBy(pathStartIndex); | |
| const endPos = startPos.getShiftedBy(pathText.length); | |
| linksToUpdate.push({ | |
| range: writer.createRange(startPos, endPos), | |
| url: normalizePathToUrl(pathText) | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| for (const link of linksToUpdate) { | |
| writer.setAttribute('linkHref', link.url, link.range); | |
| } | |
| }); | |
| }); | |
| } | |
| async entitiesReloadedEvent({loadResults}) { | |
| if (loadResults.isNoteContentReloaded(this.noteId)) { | |
| this.refresh(); | |
| } | |
| } | |
| doRender() { | |
| this.$widget = $(``); | |
| return this.$widget; | |
| } | |
| } | |
| let widget = new PathLinkerWidget(); | |
| console.log("Loaded PathLinkerWidget"); | |
| module.exports = widget; |
I'm not sure, at work we haven't updated yet, so there was no need to try.
have you checked whether NoteContextAwareWidget was removed in a changelog?
nvm, just checked. it still exists. hmm, wierd.
@laundmo Lol, it just needs to be setup as a "JS Frontend" note instead of JS Backend
Perhaps you could update the description for future reference?
oof really? allright I'll fix it
Could not open path: %USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
Add new update for the usage of this script.
16 line: await document.PathLinkerApi.runOnBackend(async (path) => {
need update to below
16 line: await document.PathLinkerApi.runAsyncOnBackendWithManualTransactionHandling(async (path) => {
This update is due to trilium app (win 0.62.5) notice me like below once i double click on the file link line.
ERROR: You're passing an async function to api.runOnBackend() which will likely not work as you intended. Either make the function synchronous (by removing 'async' keyword), or use api.runAsyncOnBackendWithManualTransactionHandling()
add the enhanced code below, which no need the special format for the file path
async function OpenLocalPath(event) {
// Check if the event was a Ctrl+double click.
if (event.ctrlKey) {
var path = event.target.innerText.trim();
console.log(>> ctrl + double_click get line content:\n${path})
//------ Check if the text content contain a file path. ------
if (/[A-Z]:\./.test(path)) {
var path = path.match(/[A-Z]:\./)[0]
// --- replace the special string
path = path.replace("USER",require('os').userInfo().username)
console.log(`>> Open local path: ${path}`)
// Use the PathLinkerApi to open the file at the path.
await document.PathLinkerApi.runAsyncOnBackendWithManualTransactionHandling(async (path) => {
const shell = require('electron').shell;
await shell.openPath(path);
return;
}, [path]);
}
}
}
const TEMPLATE = <div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> <script> ${OpenLocalPath.toString()} document.addEventListener('dblclick', OpenLocalPath); </script> </div>;
class PathLinkerWidget extends api.NoteContextAwareWidget {
constructor(...args) {
super(...args);
this.balloonEditorCreate = null;
}
get position() {
// higher value means position towards the bottom/right
return 100;
}
get parentWidget() { return 'center-pane'; }
doRender() {
this.$widget = $(TEMPLATE);
this.$widget.hide();
// store API in document to access it from callback
document.PathLinkerApi = api;
return this.$widget;
}
}
let widget = new PathLinkerWidget();
console.log(">> Loaded PathLinkerWidget");
module.exports = widget;
I'm unable to get either of these working. I get an error about "require" not being defined, using Trilium Next 0.90.7
frontend_script_api.js:203 Uncaught (in promise) Error: server error: require is not defined
at x.__runOnBackendInner (frontend_script_api.js:203:19)
at async x.runAsyncOnBackendWithManualTransactionHandling (frontend_script_api.js:243:36)
at async HTMLDocument.onClickOpenPath (<anonymous>:9:13)
same here. seems like an update of electron broke it? tried to debug but my skills aren´t good enough
I just updated this with a far superior version, fixed for trilium-next, which inserts as file:// links properly. It should "just work" without anything like italics being required. code blocks, inline or not, are excluded.
The file:// property does not work for me (Ubuntu 24.04). It does work in any browser (Firefox, Edge, Chrome) on my computer, which suggests it is a limitation of Trilium?
The file:// property does not work for me (Ubuntu 24.04). It does work in any browser (Firefox, Edge, Chrome) on my computer, which suggests it is a limitation of Trilium?
More of a bug, one i've actually investigated and found a solution for. Now its up to trilium maintainers to either merge my PR: TriliumNext/Trilium#7737 or fix it some other way.
Hi,
Based on laundmo’s PathLinkerWidget, I put together a small companion widget that opens custom-protocol URLs (e.g. bookxnotepro://..., obsidian://..., zoom://...) on double-click, using the system’s default handler via shell.openExternal.
On double-click, the code extracts the actual custom protocol URL from the double-clicked text and passes it to the backend shell.openExternal to open.
Key points: Double-click any text that contains a custom-protocol URL to open it.
It works well for me. Optionally require Ctrl+double-click by uncommenting the event.ctrlKey check.
Uses the same #widget / “JS Frontend” setup as the original gist.
/*
* Custom Protocol Opener Widget
*
* Opens custom-protocol URLs (e.g. bookxnotepro://..., obsidian://..., zoom://...)
* on double-click via the system’s default handler.
*
* Usage:
* - Add as a “JS Frontend” note and give it the #widget attribute.
* - Double-click any text that contains a custom-protocol URL to open it.
* - Optionally require Ctrl+double-click by uncommenting the event.ctrlKey check.
*
* Based on laundmo’s PathLinkerWidget:
* https://gist.github.com/laundmo/de1f386fac9f9e797fd77022d63967c9
*/
class CustomProtocolOpenerWidget extends api.NoteContextAwareWidget {
constructor(...args) {
super(...args);
}
get position() {
return 100;
}
get parentWidget() {
return 'center-pane';
}
doRender() {
this.$widget = $('<div style="display: none;"></div>');
// Global double-click handler
$(document).on('dblclick.customProtocolOpener', async (event) => {
// Uncomment the next line to require Ctrl+double-click:
// if (!event.ctrlKey) return;
// Get text near the double-clicked position
let text = '';
if (event.target && event.target.innerText) {
text = event.target.innerText;
} else if (event.target && event.target.textContent) {
text = event.target.textContent;
}
if (!text) return;
// Match custom-protocol URLs: scheme://...
const urlMatch = text.match(/([a-z][a-z0-9+.-]*:\/\/[^\s]+)/i);
if (urlMatch && urlMatch[1]) {
const url = urlMatch[1];
console.log(`CustomProtocolOpener: Detected double-click, opening URL: ${url}`);
try {
// Open URL via backend using shell.openExternal
await api.runAsyncOnBackendWithManualTransactionHandling(async (url) => {
const { shell } = require('electron');
await shell.openExternal(url);
}, [url]);
} catch (e) {
console.error('CustomProtocolOpener: Failed to open URL', e);
}
}
});
return this.$widget;
}
}
let widget = new CustomProtocolOpenerWidget();
console.log('Loaded CustomProtocolOpenerWidget');
module.exports = widget;
@laundmo I feel like i may have asked this somewhere in the past, but i can't seem to get this script to work. Do you know if it's still relevant in recent Trilium release versions?
I did set the note to
JS Backendand added the#widgetattributeI receive the following error when trying to run the script: