Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 });
Expand Down Expand Up @@ -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);
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 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();
                    }
                }

}
}
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

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.

Suggested change
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);
}
}
}

});

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));
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

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.

Suggested change
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();
}
});

}
});

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("");
Expand Down
Loading