-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat(desktop): add trilium:// custom protocol handler and --open-note CLI arg #9153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,76 @@ import port from "@triliumnext/server/src/services/port.js"; | |||||||||||||||||||||||||||||||||||||||||||||
| import { join } from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { deferred, LOCALES } from "../../../packages/commons/src"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Parses a `trilium://` protocol URL and returns the note ID, or null if the | ||||||||||||||||||||||||||||||||||||||||||||||
| * URL cannot be parsed. | ||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||
| * Supported formats: | ||||||||||||||||||||||||||||||||||||||||||||||
| * trilium://note/<noteId> | ||||||||||||||||||||||||||||||||||||||||||||||
| * trilium://<noteId> (legacy / shorthand) | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function parseTriliumUrl(rawUrl: string): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed: URL; | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| parsed = new URL(rawUrl); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.protocol !== "trilium:") { | ||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // trilium://note/<noteId> → hostname = "note", pathname = "/<noteId>" | ||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.hostname === "note") { | ||||||||||||||||||||||||||||||||||||||||||||||
| const noteId = parsed.pathname.replace(/^\/+/, "").trim(); | ||||||||||||||||||||||||||||||||||||||||||||||
| return noteId || null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // trilium://<noteId> → hostname = "<noteId>" | ||||||||||||||||||||||||||||||||||||||||||||||
| const noteId = parsed.hostname.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||
| return noteId || null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Extracts a `trilium://` URL from a process argv / commandLine array, or | ||||||||||||||||||||||||||||||||||||||||||||||
| * returns null if none is present. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function extractTriliumUrlFromArgs(args: string[]): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||
| for (const arg of args) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (arg.startsWith("trilium://")) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return arg; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| // --open-note=<noteId> convenience flag | ||||||||||||||||||||||||||||||||||||||||||||||
| const m = arg.match(/^--open-note=(.+)$/); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (m) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return `trilium://note/${m[1]}`; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Focuses the main window and navigates to the given note. | ||||||||||||||||||||||||||||||||||||||||||||||
| * Safe to call before the window is created; returns false if navigation was | ||||||||||||||||||||||||||||||||||||||||||||||
| * not possible (e.g. window not yet ready). | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function navigateToNote(noteId: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||
| const win = windowService.getLastFocusedWindow() ?? windowService.getMainWindow(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!win || win.isDestroyed()) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (win.isMinimized()) { | ||||||||||||||||||||||||||||||||||||||||||||||
| win.restore(); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| win.show(); | ||||||||||||||||||||||||||||||||||||||||||||||
| win.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| win.webContents.send("openInSameTab", noteId); | ||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||||||||||||||||||
| const userDataPath = getUserData(); | ||||||||||||||||||||||||||||||||||||||||||||||
| app.setPath("userData", userDataPath); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,6 +94,11 @@ async function main() { | |||||||||||||||||||||||||||||||||||||||||||||
| process.exit(0); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Register trilium:// as a custom URI scheme so external apps and the OS | ||||||||||||||||||||||||||||||||||||||||||||||
| // can launch Trilium and navigate directly to a note. | ||||||||||||||||||||||||||||||||||||||||||||||
| // Must be called before app.requestSingleInstanceLock(). | ||||||||||||||||||||||||||||||||||||||||||||||
| app.setAsDefaultProtocolClient("trilium"); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Adds debug features like hotkeys for triggering dev tools and reload | ||||||||||||||||||||||||||||||||||||||||||||||
| electronDebug(); | ||||||||||||||||||||||||||||||||||||||||||||||
| electronDl({ saveAs: true }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,13 +138,49 @@ async function main() { | |||||||||||||||||||||||||||||||||||||||||||||
| await serverInitializedPromise; | ||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Starting Electron..."); | ||||||||||||||||||||||||||||||||||||||||||||||
| await onReady(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Handle protocol URL passed when this is the *first* instance | ||||||||||||||||||||||||||||||||||||||||||||||
| // (Windows / Linux deliver the URL as a command-line argument). | ||||||||||||||||||||||||||||||||||||||||||||||
| const protocolUrl = extractTriliumUrlFromArgs(process.argv); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (protocolUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const noteId = parseTriliumUrl(protocolUrl); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (noteId) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Wait a short moment for the renderer to finish initialising | ||||||||||||||||||||||||||||||||||||||||||||||
| // before sending the navigation request. | ||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => navigateToNote(noteId), 1500); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On macOS, when the application is launched via a protocol URL, the URL is typically passed both in
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| app.on("will-quit", () => { | ||||||||||||||||||||||||||||||||||||||||||||||
| globalShortcut.unregisterAll(); | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // On macOS, protocol URLs for a *running* instance are delivered via | ||||||||||||||||||||||||||||||||||||||||||||||
| // the "open-url" event instead of "second-instance". | ||||||||||||||||||||||||||||||||||||||||||||||
| app.on("open-url", (event, url) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const noteId = parseTriliumUrl(url); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (noteId) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (navigateToNote(noteId)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| // Window not ready yet – retry after the ready event fires. | ||||||||||||||||||||||||||||||||||||||||||||||
| app.once("ready", () => setTimeout(() => navigateToNote(noteId), 1500)); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the other
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| app.on("second-instance", (event, commandLine) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Check if a trilium:// URL or --open-note flag was supplied. | ||||||||||||||||||||||||||||||||||||||||||||||
| const protocolUrl = extractTriliumUrlFromArgs(commandLine); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (protocolUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const noteId = parseTriliumUrl(protocolUrl); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (noteId) { | ||||||||||||||||||||||||||||||||||||||||||||||
| navigateToNote(noteId); | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const lastFocusedWindow = windowService.getLastFocusedWindow(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (commandLine.includes("--new-window")) { | ||||||||||||||||||||||||||||||||||||||||||||||
| windowService.createExtraWindow(""); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a fixed
setTimeoutto wait for the renderer to initialize is brittle and can lead to a race condition. If the app takes longer than 1.5 seconds to initialize on a slower machine or under heavy load, the navigation will fail. A more robust approach is to wait for the window's content to load by listening for thedid-finish-loadevent onwebContents. This ensures the navigation command is sent at a more appropriate time.