fix(electron): require TerminalEmulator category for terminal appId on Linux

desktopEntryMatchesApp used a loose substring match that accepted any
desktop entry whose Exec line mentioned a terminal launcher. A non-
terminal app launched via xdg-terminal-exec (e.g. a TUI helper) was
mis-attributed to the generic 'terminal' appId, so 'Open in Terminal'
launched the helper script and the picker showed the helper's icon.

For appId === 'terminal' only, require Categories=TerminalEmulator;
ghostty/iterm2 keep name-based matching. The xdg-terminal-exec /
gnome-terminal / konsole / xfce4-terminal / x-terminal-emulator fallback
chain is unchanged, so non-conformant custom terminal entries still
launch. Adds a shared isTerminalEmulatorEntry predicate used by both
buildLinuxOpenSpecs (launch) and buildLinuxInstalledApps (icon).

Smoke test gains a generic helper-app fixture (non-terminal app whose
Exec uses xdg-terminal-exec) plus a TerminalEmulator entry, with
assertions that the helper script is never launched and the terminal
emulator's icon resolves instead of the helper's. Two byte-distinct
PNGs keep the icon assertion a real discriminator.
This commit is contained in:
Pablo
2026-08-03 18:08:24 +02:00
parent 1cc5cfedbb
commit a40654dfbb
2 changed files with 79 additions and 3 deletions
+9 -2
View File
@@ -234,6 +234,11 @@ const commandExists = (program, env = process.env) => {
const findEntry = (entries, appId, appName) => entries.find((entry) => desktopEntryMatchesApp(entry, appName, appId)) || null;
const isTerminalEmulatorEntry = (entry) => {
const categories = Array.isArray(entry?.categories) ? entry.categories : [];
return categories.some((category) => normalizeComparable(category) === 'terminalemulator');
};
export const buildLinuxOpenSpecs = ({ targetPath, appId, appName, targetKind = 'path', entries = [], env = process.env }) => {
if (appId === 'finder') {
return [{ kind: 'default', targetKind, targetPath }];
@@ -241,7 +246,9 @@ export const buildLinuxOpenSpecs = ({ targetPath, appId, appName, targetKind = '
const specs = [];
if (TERMINAL_APP_IDS.has(appId)) {
const directory = targetKind === 'file' ? path.dirname(targetPath) : targetPath;
const terminalEntry = findEntry(entries, appId, appName);
const terminalEntry = appId === 'terminal'
? entries.find(isTerminalEmulatorEntry) || null
: findEntry(entries, appId, appName);
if (terminalEntry) {
const spec = buildCommandFromDesktopExec(terminalEntry, directory);
if (spec) specs.push(spec);
@@ -515,7 +522,7 @@ export const buildLinuxInstalledApps = async (apps, options = {}) => {
...FILE_MANAGER_ICON_FALLBACKS,
], { ...options, env });
} else if (normalizeComparable(name) === 'terminal') {
const terminalEntry = findEntry(entries, 'terminal', name)
const terminalEntry = entries.find(isTerminalEmulatorEntry)
|| findEntry(entries, 'ghostty', 'Ghostty');
iconDataUrl = resolveIconDataUrlForName([
terminalEntry?.icon,