feat(desktop): add pure auto-launch helpers with tests
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
deriveStartMinimizedFromArgs,
|
||||||
|
parseExecPathFromDesktopFile,
|
||||||
|
shouldReapplyAppImage,
|
||||||
|
} from './autoLaunch';
|
||||||
|
|
||||||
|
describe('deriveStartMinimizedFromArgs', () => {
|
||||||
|
it('returns true when --hidden is present', () => {
|
||||||
|
expect(deriveStartMinimizedFromArgs(['--hidden'])).toBe(true);
|
||||||
|
expect(deriveStartMinimizedFromArgs(['--foo', '--hidden', '--bar'])).toBe(true);
|
||||||
|
});
|
||||||
|
it('returns false when --hidden is absent', () => {
|
||||||
|
expect(deriveStartMinimizedFromArgs([])).toBe(false);
|
||||||
|
expect(deriveStartMinimizedFromArgs(['--other'])).toBe(false);
|
||||||
|
});
|
||||||
|
it('handles undefined safely', () => {
|
||||||
|
expect(deriveStartMinimizedFromArgs(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseExecPathFromDesktopFile', () => {
|
||||||
|
it('extracts an unquoted path', () => {
|
||||||
|
const content = [
|
||||||
|
'[Desktop Entry]',
|
||||||
|
'Type=Application',
|
||||||
|
'Name=Backspace',
|
||||||
|
'Exec=/opt/Backspace/backspace --hidden',
|
||||||
|
'X-GNOME-Autostart-enabled=true',
|
||||||
|
].join('\n');
|
||||||
|
expect(parseExecPathFromDesktopFile(content)).toBe('/opt/Backspace/backspace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts a double-quoted path (handles spaces in path)', () => {
|
||||||
|
const content = 'Exec="/home/user/Apps/Backspace 1.0.AppImage" --hidden\n';
|
||||||
|
expect(parseExecPathFromDesktopFile(content)).toBe('/home/user/Apps/Backspace 1.0.AppImage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when no Exec= line is present', () => {
|
||||||
|
expect(parseExecPathFromDesktopFile('[Desktop Entry]\nName=X\n')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores commented Exec lines', () => {
|
||||||
|
expect(parseExecPathFromDesktopFile('#Exec=/wrong\nExec=/right\n')).toBe('/right');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null on empty input', () => {
|
||||||
|
expect(parseExecPathFromDesktopFile('')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldReapplyAppImage', () => {
|
||||||
|
it('returns true when AppImage path differs from recorded path', () => {
|
||||||
|
expect(shouldReapplyAppImage('/home/u/Backspace-2.0.AppImage', '/home/u/Backspace-1.0.AppImage')).toBe(true);
|
||||||
|
});
|
||||||
|
it('returns false when paths match', () => {
|
||||||
|
expect(shouldReapplyAppImage('/home/u/Backspace-2.0.AppImage', '/home/u/Backspace-2.0.AppImage')).toBe(false);
|
||||||
|
});
|
||||||
|
it('returns false when no recorded path exists — treat missing autostart entry as user-disabled', () => {
|
||||||
|
// A missing .desktop file is a user signal (they disabled via their DE's startup manager
|
||||||
|
// or removed it manually), not a stale-path-needs-refresh signal. The OS-authoritative
|
||||||
|
// architecture must treat this as "OS state changed, do not override".
|
||||||
|
expect(shouldReapplyAppImage('/home/u/Backspace.AppImage', null)).toBe(false);
|
||||||
|
});
|
||||||
|
it('returns false when there is no current AppImage env (not in AppImage runtime)', () => {
|
||||||
|
expect(shouldReapplyAppImage(null, '/anything')).toBe(false);
|
||||||
|
expect(shouldReapplyAppImage(null, null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
export function deriveStartMinimizedFromArgs(args: string[] | undefined): boolean {
|
||||||
|
if (!args) return false;
|
||||||
|
return args.includes('--hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the executable path from a freedesktop .desktop file's Exec= line.
|
||||||
|
* Handles double-quoted paths (which may contain spaces). Ignores commented
|
||||||
|
* lines (leading #). Returns null if no Exec= line is present or parseable.
|
||||||
|
*
|
||||||
|
* The .desktop spec also defines field codes (%f, %u, etc.) and backslash
|
||||||
|
* escapes; we don't need to honour them here because we only ever compare the
|
||||||
|
* leading executable token, never re-execute it.
|
||||||
|
*/
|
||||||
|
export function parseExecPathFromDesktopFile(content: string): string | null {
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
for (const raw of lines) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line || line.startsWith('#')) continue;
|
||||||
|
if (!line.startsWith('Exec=')) continue;
|
||||||
|
const value = line.slice('Exec='.length).trimStart();
|
||||||
|
if (!value) continue;
|
||||||
|
if (value.startsWith('"')) {
|
||||||
|
const end = value.indexOf('"', 1);
|
||||||
|
if (end === -1) return null;
|
||||||
|
return value.slice(1, end);
|
||||||
|
}
|
||||||
|
const sp = value.indexOf(' ');
|
||||||
|
return sp === -1 ? value : value.slice(0, sp);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true iff the existing autostart entry's recorded executable path differs
|
||||||
|
* from the current AppImage path (i.e., the AppImage moved and the entry is stale).
|
||||||
|
*
|
||||||
|
* A missing recorded path (recordedExecPath === null, i.e. the .desktop file does
|
||||||
|
* not exist) is intentionally treated as "do nothing" — it represents the user
|
||||||
|
* disabling autostart via their desktop environment's startup manager or removing
|
||||||
|
* the file manually. Re-creating it would override OS-level user intent, which is
|
||||||
|
* exactly the bug class this whole refactor exists to fix.
|
||||||
|
*/
|
||||||
|
export function shouldReapplyAppImage(
|
||||||
|
currentAppImagePath: string | null,
|
||||||
|
recordedExecPath: string | null,
|
||||||
|
): boolean {
|
||||||
|
if (!currentAppImagePath) return false;
|
||||||
|
if (!recordedExecPath) return false;
|
||||||
|
return currentAppImagePath !== recordedExecPath;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user