Linux Integration
Aartiq for Linux
Linux Integration
Implemented in src/lib/linux-integration.js. Twelve actions, an eleven-channel bridge, and three ways of writing .desktop files. Four actions reach a real system API. Eight send on an IPC channel that no renderer subscribes to, and four of the eleven bridge methods ask for a channel that has no handler at all.
aartiq:// links are discarded on Linux
The module registers the scheme at startup via registerLinuxProtocol(), and main.js wires up the eleven IPC handlers behind process.platform === 'linux'. Both of those work.
What does not exist is the delivery path. handleLinuxURLScheme is the function that would receive an activated link, and it is imported by main.js and never called. There is no second-instance handler and nothing reads process.argv for a URL, which is how XDG delivers a protocol activation.
The actions are reachable from inside the app through electronAPI.linux.executeAction, and from nowhere else. The Windows page documents the identical defect, and the deep-link reference explains why macOS is the only platform where a link does anything.
Actions
All twelve entries from the actionHandlers table. The reach column is derived by a test from whether the handler's channel has a subscriber, so it cannot drift from the source.
aartiq://open-app?appName=…Runs gtk-launch on GNOME and KDE, falling back to kioclient, and xdg-open everywhere else. Because it is exec'd as a string, the appName parameter is interpolated into a shell command line.
appNameaartiq://volume?level (0-100)=…pactl on GNOME, qdbus against org.kde.KMix on KDE, and an explicit error on any other desktop. Note that the desktop is matched by exact string equality against a lowercased XDG_CURRENT_DESKTOP, so Ubuntu's usual "ubuntu:GNOME" does not match "gnome" and silently takes the unsupported branch.
level (0-100)aartiq://voice?text=…When speak is not the string "false" it calls speakText, which runs espeak. This is output only; there is no dictation path. startVoiceRecognition returns success: false and points at whisper.cpp.
text, speakaartiq://notify?title=…notify-send on GNOME, kdialog --passivepopup on KDE, and notify-send with a five-second timeout everywhere else. The title and message are interpolated into the notify-send command string.
title, message, iconaartiq://chat?message=…Sends ai:chat-message. Nothing subscribes to that channel.
messageaartiq://navigate?url=…Sends browser:navigate. Nothing subscribes to that channel.
urlaartiq://search?query=…Sends ai:search. Nothing subscribes to that channel.
queryaartiq://create-pdf?content=…Sends ai:create-pdf. Nothing subscribes to that channel.
content, titleaartiq://run-command?command=…Sends ai:run-command and returns "Command queued". Nothing subscribes to that channel, so nothing runs. There is no confirmation gate on this path, unlike the Windows one.
commandaartiq://screenshot?mode (default fullscreen)=…Sends ai:screenshot. Nothing subscribes to that channel. The previous version of this page claimed capture via scrot or import; the module invokes no screenshot program at all.
mode (default fullscreen), pathaartiq://schedule?task=…Sends ai:schedule. Nothing subscribes to that channel.
task, cronaartiq://ask-ai?prompt=…Sends ai:ask-ai. Nothing subscribes to that channel.
prompt, speakBridge API
The preload bridge calls 11 channels and main.js registers 11, so every method has a handler and every name matches. The Windows bridge, likewise, matches on all nine of its names. That part is wired correctly. What follows is not.
| Channel | Arguments | Handler |
|---|---|---|
| linux:execute-action | action, params | Runs any action in the table above |
| linux:desktop:get | (none) | Current desktop, from XDG_CURRENT_DESKTOP |
| linux:notify | title, body, options | Desktop notification |
| linux:voice:listen | params | Dictation — always returns success: false |
| linux:voice:speak | text, params | Speech synthesis through espeak |
| linux:voice:get-voices | (none) | espeak --voices |
| linux:generate-url | action, params | Builds an aartiq:// URL |
| linux:create-shortcut | name, action, params | Writes a .desktop file into userData |
| linux:install-gnome-shortcut | name, action, params | Writes into ~/.local/share/applications |
| linux:create-launcher | (none) | Writes aartiq.desktop into userData |
| linux:register-protocol | (none) | Registers aartiq:// for this build |
The Linux startup crash is fixed
This page used to report that the app could not start on Linux at all. setupLinuxIPCHandlers() registered five linux: channels that main.js then registered again at module scope. Electron's ipcMain.handle throws on a second registration, the call was not wrapped, and it ran at module top level — so on Linux the main process stopped before the window was created. macOS and Windows were unaffected, because the setup call sits inside a process.platform === 'linux' guard, which is why it survived as long as it did.
Each channel is now registered exactly once. The five main.js copies were kept rather than the module's, and that choice matters: they carry a process.platform !== 'linux' check that answers { error: 'Not Linux' } on the other two platforms. Removing those instead would have left every call to them rejecting with No handler registered instead of returning an error.
A test calls the real setup function against an Electron stub whose ipcMain.handle throws on a duplicate, exactly as the real one does, and requires the registration not to throw. That test could not exist before: the module named a parameter interface, which is a reserved word in strict mode, so Jest could not load the file at all.
The orphan list is a separate finding and the fix did not change it: those five channels are still registered by the module and still invoked by nothing in the repository. Fixing the crash removed the duplication, not the dead channels.
Calling an action from inside the app
Subject to the caveat above, this is the path that works. It goes through the preload bridge, so it inherits the same dead-channel problem for the eight actions that only send.
// The one path that works on Linux
await window.electronAPI.linux.executeAction('notify', {
title: 'Build finished',
message: 'The app is ready',
});
// -> { success: true } and a real notification appears
await window.electronAPI.linux.executeAction('chat', { message: 'hello' });
// -> { success: true, message: 'Message sent to AI' } …and nothing happens
// Synthesis works; dictation does not.
await window.electronAPI.linux.voice.speak('Done');
await window.electronAPI.linux.voice.listen();
// -> { success: false, message: '... whisper.cpp ...' }Desktop entries
Three bridge methods write .desktop files. Each has a problem that stops the file it writes from doing anything, which is worth knowing before treating this as a working feature.
createLinuxShortcut writes an invalid Exec line
The generated .desktop file sets Exec to the aartiq:// URL itself. The desktop entry specification requires Exec to name an executable, so a launcher created this way will not run. The file is written into the app's userData directory rather than a directory the desktop shell searches, which compounds it.
installGNOMEShortcut assumes an aartiq binary is on PATH
It writes Exec=aartiq "<url>", but no binary of that name is installed by the project. It also accepts desktop === 'ubuntu' as a synonym for GNOME, which is the opposite of the exact-match rule the volume and notify handlers use.
createDesktopLauncher registers the scheme where nothing reads it
It is the only launcher that sets Exec correctly and includes MimeType=x-scheme-handler/aartiq, but it writes into userData instead of ~/.local/share/applications, so the desktop never discovers the association.
Claims removed from this page
- —System tray support, for both GNOME and KDE. There is no tray code in this module.
- —KRunner integration. Nothing in the repository references KRunner.
- —GNOME and Plasma global keybindings. The module registers no global shortcut; keybinding registration lives in main.js and is not part of this integration.
- —Screenshot capture via scrot or import. The screenshot action sends an IPC message and runs no capture program.
- —pocketsphinx as an offline speech-to-text dependency, and web-based STT via the AI backend as an implemented path. startVoiceRecognition returns success: false and names whisper.cpp in its message.
- —Voice input, or dictating messages to AI. There is no dictation path on Linux.
- —"Choose from 80+ eSpeak voices" as a product feature. espeak is real and the voice list is read from espeak --voices, but the number was asserted rather than measured and depends entirely on the installed voice package.
- —The screenshot row was missing from the action table on the old page even though screenshot is a real action, and the table omitted the voice action.
- —Global Shortcuts as a feature of desktop integration, which implies a system-wide hotkey that does not exist.