Skip to content

Add URL protocol support for trilium://#9248

Closed
zicaiw625 wants to merge 1 commit into
TriliumNext:mainfrom
zicaiw625:add-url-protocol-support
Closed

Add URL protocol support for trilium://#9248
zicaiw625 wants to merge 1 commit into
TriliumNext:mainfrom
zicaiw625:add-url-protocol-support

Conversation

@zicaiw625
Copy link
Copy Markdown

@zicaiw625 zicaiw625 commented Apr 1, 2026

Add URL protocol support for trilium://

Description

This PR adds URL protocol support for Trilium Notes, allowing users to open notes via trilium:// URLs from external applications.

Changes Made

1. Desktop Main Process (apps/desktop/src/main.ts)

  • Registered trilium:// as a default protocol client
  • Added handling for URL protocol in second-instance event
  • Added initial launch URL protocol handling
  • Sends IPC message open-note-by-id to renderer process

2. Client Renderer Process (apps/client/src/index.ts)

  • Added IPC listener for open-note-by-id messages
  • Imports note context service to open the specified note
  • Only activates in Electron environment

Usage Examples

  1. Register URL protocol (automatically done on app launch):

    # On first launch, Trilium registers itself as handler for trilium://
  2. Open note from command line:

    trilium://note123
  3. Use in external applications:

    • Task managers can store trilium://note123 links
    • Clicking the link opens the note in Trilium
    • If Trilium is already running, it focuses the window and opens the note

Technical Details

  • URL format: trilium://{noteId}
  • Note ID extraction: Removes trilium:// prefix and any leading slashes
  • Cross-platform: Works on Windows, macOS, and Linux
  • Single instance: Uses Electron's requestSingleInstanceLock for proper handling

Testing

  • Tested URL parsing logic with various input formats
  • IPC communication between main and renderer processes
  • Proper window focus behavior when opening via URL

Related Issue

Fixes #649 - Open/focus a note from command line / desktop URL handler


IssueHunt Summary

Referenced issues

This pull request has been submitted to:


- Register trilium:// as default protocol client
- Handle URL protocol in second-instance event
- Add IPC listener in client to open notes by ID
- Fixes TriliumNext#649 - Open/focus a note from command line / desktop URL handler
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Apr 1, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for the trilium:// URL protocol, enabling the application to open specific notes from external links in both new and existing instances. The changes involve registering the protocol in the main process and setting up IPC listeners in the renderer. Feedback focuses on improving the robustness of the implementation by addressing a brittle setTimeout used for window readiness, removing a redundant single-instance lock check, and refactoring fragile, duplicated URL parsing logic. Additionally, it is recommended to avoid using the global object for state management to ensure better type safety.

Comment thread apps/desktop/src/main.ts
Comment on lines +167 to +171
setTimeout(() => {
mainWindow.webContents.send("open-note-by-id", global.noteIdToOpen);
delete global.noteIdToOpen;
}, 1000); // Wait a bit for window to be ready
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using a hardcoded setTimeout of 1000ms to wait for the renderer process is brittle and creates a race condition. On slower systems or during heavy initialization (like database migrations), the renderer might not be ready in time to receive the IPC message, causing the note to fail to open. A more robust solution would be to have the renderer signal its readiness via IPC once the listener is registered, or at least use the dom-ready event of the webContents.

Comment thread apps/desktop/src/main.ts
Comment on lines +76 to +78
const urlArg = commandLine.find(arg => arg.startsWith("trilium://"));
if (urlArg) {
const noteId = urlArg.replace("trilium://", "").replace(/^\/\//, "");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The URL parsing logic here is fragile and duplicated. Specifically, the second .replace(/^\/\//, "") will not work as intended because the first .replace("trilium://", "") already consumes the double slashes. This would leave a leading slash if the URL is formatted as trilium:///noteId. Additionally, protocol matching should ideally be case-insensitive to handle various OS behaviors.

Suggested change
const urlArg = commandLine.find(arg => arg.startsWith("trilium://"));
if (urlArg) {
const noteId = urlArg.replace("trilium://", "").replace(/^\/\//, "");
const urlArg = commandLine.find(arg => /^trilium:\/\//i.test(arg));
if (urlArg) {
const noteId = urlArg.replace(/^trilium:\/+/i, "");

Comment thread apps/desktop/src/main.ts
Comment on lines +143 to +147
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This call to app.requestSingleInstanceLock() is redundant. The single instance lock is already requested and handled at the beginning of the main() function (lines 107-111). If the lock is not acquired, the application process exits before reaching the onReady function.

Comment thread apps/desktop/src/main.ts
Comment on lines +150 to +152
const urlArg = process.argv.find(arg => arg.startsWith("trilium://"));
if (urlArg) {
const noteId = urlArg.replace("trilium://", "").replace(/^\/\//, "");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This parsing logic is duplicated from the second-instance handler and shares the same issues regarding leading slashes and case sensitivity.

Suggested change
const urlArg = process.argv.find(arg => arg.startsWith("trilium://"));
if (urlArg) {
const noteId = urlArg.replace("trilium://", "").replace(/^\/\//, "");
const urlArg = process.argv.find(arg => /^trilium:\/\//i.test(arg));
if (urlArg) {
const noteId = urlArg.replace(/^trilium:\/+/i, "");

Comment thread apps/desktop/src/main.ts
const noteId = urlArg.replace("trilium://", "").replace(/^\/\//, "");
console.log(`Initial launch with note ID: ${noteId}`);
// Store note ID to be used after window is created
global.noteIdToOpen = noteId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the global object to store application state is generally discouraged in TypeScript as it lacks type safety and can lead to naming collisions. A module-level variable (e.g., let noteIdToOpen: string | null = null;) at the top of the file would be a cleaner and more type-safe approach.

@eliandoran
Copy link
Copy Markdown
Contributor

Hi,

Unfortunately I can't merge this due to design issues as highlighted by Gemini (use of timeouts being one notable example). We'd prefer to manage this core feature on our side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open/focus a note from command line / desktop URL handler (Trilium URL protocol)

2 participants