-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat: register trilium:// custom protocol handler for deep linking #9203
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import windowService from "@triliumnext/server/src/services/window.js"; | ||
|
|
||
| const PROTOCOL = "trilium"; | ||
|
|
||
| /** Note ID to navigate to once the main window finishes loading. */ | ||
| let pendingNoteId: string | null = null; | ||
|
|
||
| /** | ||
| * Parses a `trilium://` URL and returns the note ID, or `null` if the URL | ||
| * cannot be parsed. | ||
| * | ||
| * Supported formats: | ||
| * trilium://note/<noteId> (canonical) | ||
| * trilium://<noteId> (shorthand) | ||
| */ | ||
| function parseTriliumUrl(rawUrl: string): string | null { | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(rawUrl); | ||
| } catch { | ||
| return null; | ||
| } | ||
|
|
||
| if (parsed.protocol !== `${PROTOCOL}:`) { | ||
| 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; | ||
| } | ||
|
Comment on lines
+16
to
+39
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. Please add unit tests. Make sure to cover the case indicated by the Copilot comment from two eweeks ago. |
||
|
|
||
| /** | ||
| * Scans process arguments for a `trilium://` URL or `--open-note=<noteId>` | ||
| * flag and returns the target note ID. | ||
| */ | ||
| function extractNoteIdFromArgs(args: string[]): string | null { | ||
| for (const arg of args) { | ||
| if (arg.startsWith(`${PROTOCOL}://`)) { | ||
| return parseTriliumUrl(arg); | ||
| } | ||
|
|
||
| const match = arg.match(/^--open-note=(.+)$/); | ||
| if (match) { | ||
| return match[1].trim() || null; | ||
| } | ||
| } | ||
|
Comment on lines
+45
to
+60
|
||
| return null; | ||
| } | ||
|
Comment on lines
+41
to
+62
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. Same here, write tests + check case from Copilot. |
||
|
|
||
| /** | ||
| * Focuses the appropriate window and sends navigation IPC to the renderer. | ||
| * Waits for `did-finish-load` if the page is still loading (first launch). | ||
| * | ||
| * Returns `false` when no usable window exists yet. | ||
| */ | ||
| 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(); | ||
|
|
||
| if (win.webContents.isLoading()) { | ||
| win.webContents.once("did-finish-load", () => { | ||
| win.webContents.send("openInSameTab", noteId); | ||
| }); | ||
| } else { | ||
| win.webContents.send("openInSameTab", noteId); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Handles an incoming protocol URL (from `open-url`, `second-instance`, or | ||
| * first-launch argv). If the main window is not yet available the note ID is | ||
| * queued for {@link processPendingNavigation}. | ||
| */ | ||
| function handleProtocolUrl(url: string): void { | ||
| const noteId = parseTriliumUrl(url); | ||
| if (!noteId) { | ||
| return; | ||
| } | ||
|
|
||
| if (!navigateToNote(noteId)) { | ||
| // Window not created yet — defer until after onReady(). | ||
| pendingNoteId = noteId; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Navigates to the note that was requested before the window was ready, then | ||
| * clears the pending state. Safe to call when there is nothing pending. | ||
| */ | ||
| function processPendingNavigation(): void { | ||
| if (pendingNoteId) { | ||
| const noteId = pendingNoteId; | ||
| pendingNoteId = null; | ||
| navigateToNote(noteId); | ||
| } | ||
| } | ||
|
|
||
| export default { | ||
| PROTOCOL, | ||
| parseTriliumUrl, | ||
| extractNoteIdFromArgs, | ||
| navigateToNote, | ||
| handleProtocolUrl, | ||
| processPendingNavigation, | ||
| }; | ||
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.
parseTriliumUrl()currently returns the fullpathname(minus leading slashes) for canonical URLs. If the URL has a trailing slash (e.g.trilium://note/<id>/) or additional path segments, the returned noteId will include/and won’t match Trilium note IDs (expected length 12). Consider normalizing by stripping trailing slashes and/or taking only the first path segment, and optionally validating against the expected noteId format before returning it.