feat(desktop): add trilium:// custom protocol handler and --open-note CLI arg#9153
feat(desktop): add trilium:// custom protocol handler and --open-note CLI arg#9153argusagent wants to merge 2 commits intoTriliumNext:mainfrom
Conversation
Register 'trilium://' as a custom URI scheme so external apps and the OS can launch TriliumNext and navigate directly to a specific note. Supported URL formats: trilium://note/<noteId> (preferred) trilium://<noteId> (shorthand / legacy-compatible) Supported CLI flag: --open-note=<noteId> Behaviour: - If TriliumNext is not running, it starts and opens the requested note once the main window is ready (~1.5 s after the ready event). - If TriliumNext is already running (single-instance lock held), the second-instance event delivers the protocol URL / CLI flag and the existing window is focused then navigated to the note. - On macOS, protocol activations for a running instance arrive via the open-url event and are handled identically. Implementation detail: navigation is performed by sending the existing 'openInSameTab' IPC message to the renderer, which calls appContext.tabManager.openInSameTab(noteId). Closes TriliumNext#649
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces deep linking capabilities to TriliumNext, allowing users to open specific notes directly from external sources. By registering a custom Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a custom protocol handler trilium:// and a CLI argument --open-note to allow direct navigation to notes. The implementation is well-structured with helper functions for parsing and navigation. However, I've identified a couple of areas for improvement regarding the handling of initial navigation. The current implementation relies on fixed timeouts, which can be unreliable. I've suggested using Electron's event-based mechanisms for a more robust solution. Additionally, there's a small logic issue on macOS where navigation could be triggered twice. My comments provide specific suggestions to address these points.
| // Wait a short moment for the renderer to finish initialising | ||
| // before sending the navigation request. | ||
| setTimeout(() => navigateToNote(noteId), 1500); |
There was a problem hiding this comment.
Using a fixed setTimeout to 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 the did-finish-load event on webContents. This ensures the navigation command is sent at a more appropriate time.
// Wait for the window to finish loading its content before navigating.
const win = windowService.getMainWindow();
if (win) {
const navigate = () => {
// A short delay might still be needed for client-side JS to initialize.
setTimeout(() => navigateToNote(noteId), 200);
};
if (win.webContents.isLoading()) {
win.webContents.once('did-finish-load', navigate);
} else {
navigate();
}
}| return; | ||
| } | ||
| // Window not ready yet – retry after the ready event fires. | ||
| app.once("ready", () => setTimeout(() => navigateToNote(noteId), 1500)); |
There was a problem hiding this comment.
Similar to the other setTimeout, using a fixed delay after the ready event is unreliable. When handling a URL for an app that isn't running yet, it's better to wait for the window to be created and its content to load. You can achieve this by listening for the browser-window-created event, and then the did-finish-load event on the new window's webContents.
| app.once("ready", () => setTimeout(() => navigateToNote(noteId), 1500)); | |
| app.once('browser-window-created', (event, win) => { | |
| const navigate = () => { | |
| // A short delay might still be needed for client-side JS to initialize. | |
| setTimeout(() => navigateToNote(noteId), 200); | |
| }; | |
| if (win.webContents.isLoading()) { | |
| win.webContents.once('did-finish-load', navigate); | |
| } else { | |
| navigate(); | |
| } | |
| }); |
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
On macOS, when the application is launched via a protocol URL, the URL is typically passed both in process.argv and through the open-url event. The current implementation handles both, which could lead to two redundant attempts to navigate to the same note. Since the open-url event is the standard and more robust way to handle this on macOS, it's best to restrict the process.argv check to other platforms like Windows and Linux to avoid this redundancy.
| 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); | |
| } | |
| } | |
| // Handle protocol URL passed when this is the *first* instance | |
| // (Windows / Linux deliver the URL as a command-line argument). | |
| if (process.platform !== 'darwin') { | |
| 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); | |
| } | |
| } | |
| } |
|
@argusagent , since this contribution feels a bit AI-heavy, please tell me which testing was done and on what platforms. |
|
Thanks for the review! Testing breakdown: Windows (primary)
macOS path
Linux
Edge cases
One thing I noticed: the 1500ms |
… guard
- Wrap process.argv check in process.platform !== 'darwin' guard (Issue 3)
- Use did-finish-load instead of setTimeout(1500) in app.on('ready') (Issue 1)
- Use browser-window-created + did-finish-load in open-url fallback (Issue 2)
|
Thanks for the thorough review! Addressed all three points in the latest commit: 1 & 3 — 2 — |
|
Thanks for the contribution. From what I see #9203 does more or less the same with some small improvements.
We are going to focus on that one instead. |
Summary
Implements a custom URI scheme ( rilium://) so that external apps, scripts, and OS integrations can launch TriliumNext and navigate directly to a specific note.
Closes #649
Supported formats
How it works
Protocol registration - �pp.setAsDefaultProtocolClient('trilium') is called before the single-instance lock so the OS registers the scheme.
New instance - when TriliumNext is not running and is launched via rilium://note/NOTEID (Windows / Linux pass the URL as a command-line argument; macOS fires open-url), the app starts normally and navigates to the note ~1.5 s after the window is ready.
Running instance - when TriliumNext is already running the second-instance event receives the command line. The protocol URL (or --open-note flag) is parsed, the existing window is focused, and openInSameTab IPC is sent to the renderer.
macOS - protocol activations for a running instance arrive via the open-url event and are handled identically.
Implementation detail
Navigation is performed by sending the pre-existing openInSameTab IPC message to the renderer, which calls �ppContext.tabManager.openInSameTab(noteId). No renderer changes are required.
Files changed
avigateToNote() helpers; wired up open-url event handler; updated second-instance handler; added �pp.setAsDefaultProtocolClient('trilium') call.