summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-12-21 15:46:15 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-12-21 15:46:15 +0100
commited63441bfc8b660aa57a6941a0f08cf2097a8291 (patch)
treed6dce1767c5ed8e1999f17b3d4ab45e9ecd93f38 /gui
parentfad33b93c1677cef630423715141389eabf837b5 (diff)
parent5b90106269ed93c8f936bda87814d6fba7c3b537 (diff)
downloadmullvadvpn-ed63441bfc8b660aa57a6941a0f08cf2097a8291.tar.xz
mullvadvpn-ed63441bfc8b660aa57a6941a0f08cf2097a8291.zip
Merge branch 'fix-xfce-window-icon'
Diffstat (limited to 'gui')
-rw-r--r--gui/src/main/index.ts13
-rw-r--r--gui/src/main/linux-desktop-entry.ts155
-rw-r--r--gui/src/main/linux-split-tunneling.ts177
-rw-r--r--gui/src/renderer/app.tsx4
-rw-r--r--gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx8
-rw-r--r--gui/src/shared/application-types.ts22
-rw-r--r--gui/src/shared/ipc-event-channel.ts6
-rw-r--r--gui/src/shared/linux-split-tunneling-application.ts9
8 files changed, 205 insertions, 189 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 2db95816a0..535d893bb8 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -62,6 +62,7 @@ import { ConnectionObserver, DaemonRpc, SubscriptionListener } from './daemon-rp
import { InvalidAccountError } from './errors';
import Expectation from './expectation';
import GuiSettings from './gui-settings';
+import { getAppIcon } from './linux-desktop-entry';
import NotificationController from './notification-controller';
import { resolveBin } from './proc';
import ReconnectionBackoff from './reconnection-backoff';
@@ -365,7 +366,7 @@ class ApplicationMain {
);
this.connectToDaemon();
- const window = this.createWindow();
+ const window = await this.createWindow();
const tray = this.createTray();
const windowController = new WindowController(window, tray, this.guiSettings.unpinnedWindow);
@@ -1340,7 +1341,7 @@ class ApplicationMain {
if (this.tray && this.windowController) {
this.tray.removeAllListeners();
- const window = this.createWindow();
+ const window = await this.createWindow();
this.windowController.replaceWindow(window, unpinnedWindow);
await this.initializeWindow();
@@ -1409,7 +1410,7 @@ class ApplicationMain {
}
}
- private createWindow(): BrowserWindow {
+ private async createWindow(): Promise<BrowserWindow> {
const contentHeight = 568;
// the size of transparent area around arrow on macOS
const headerBarArrowHeight = 12;
@@ -1475,6 +1476,12 @@ class ApplicationMain {
autoHideMenuBar: true,
});
+ case 'linux':
+ return new BrowserWindow({
+ ...options,
+ icon: await getAppIcon('mullvad-vpn'),
+ });
+
default: {
return new BrowserWindow(options);
}
diff --git a/gui/src/main/linux-desktop-entry.ts b/gui/src/main/linux-desktop-entry.ts
new file mode 100644
index 0000000000..02edd8eb0f
--- /dev/null
+++ b/gui/src/main/linux-desktop-entry.ts
@@ -0,0 +1,155 @@
+import child_process from 'child_process';
+import log from 'electron-log';
+import fs from 'fs';
+import path from 'path';
+import { ILinuxApplication } from '../shared/application-types';
+
+type DirectoryDescription = string | RegExp;
+
+// Implemented according to freedesktop specification
+// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
+// TODO: Respect "TryExec"
+export function shouldShowApplication(application: ILinuxApplication): boolean {
+ const originalXdgCurrentDesktop = process.env.ORIGINAL_XDG_CURRENT_DESKTOP?.split(':') ?? [];
+ const xdgCurrentDesktop = process.env.XDG_CURRENT_DESKTOP?.split(':') ?? [];
+ const desktopEnvironments = originalXdgCurrentDesktop.concat(xdgCurrentDesktop);
+
+ const notShowIn =
+ typeof application.notShowIn === 'string' ? [application.notShowIn] : application.notShowIn;
+ const onlyShowIn =
+ typeof application.onlyShowIn === 'string' ? [application.onlyShowIn] : application.onlyShowIn;
+
+ const notShowInMatch = notShowIn?.some((desktopEnvironment) =>
+ desktopEnvironments?.includes(desktopEnvironment),
+ );
+ const onlyShowInMatch =
+ onlyShowIn?.some((desktopEnvironment) => desktopEnvironments?.includes(desktopEnvironment)) ??
+ false;
+
+ return (
+ application.type === 'Application' &&
+ application.name !== 'Mullvad VPN' &&
+ application.exec !== undefined &&
+ application.noDisplay !== 'true' &&
+ application.terminal !== 'true' &&
+ application.hidden !== 'true' &&
+ !notShowInMatch &&
+ (!application.onlyShowIn || onlyShowInMatch)
+ );
+}
+
+export async function getAppIcon(name?: string) {
+ if (!name || path.isAbsolute(name)) {
+ return name;
+ }
+
+ // Chromium doesn't support .xpm files
+ const extensions = ['svg', 'png'];
+ return findIcon(name, extensions, [
+ getIconDirectories(),
+ await getGtkThemeDirectories(),
+ // Begin with preferred sized but if nothing matches other sizes should be considered as well.
+ ['scalable', '256x256', '512x512', '256x256@2x', '128x128@2x', '128x128', /^\d+x\d+(@2x)?$/],
+ // Search in all categories of icons.
+ [/.*/],
+ ]);
+}
+
+// Implemented according to freedesktop specification.
+// https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
+function getIconDirectories() {
+ const directories: string[] = [];
+
+ if (process.env.HOME) {
+ directories.push(path.join(process.env.HOME, '.icons'));
+ directories.push(path.join(process.env.HOME, '.local', 'share', 'icons')); // For KDE Plasma
+ }
+
+ if (process.env.XDG_DATA_DIRS) {
+ const dataDirs = process.env.XDG_DATA_DIRS.split(':').map((dir) => path.join(dir, 'icons'));
+ directories.push(...dataDirs);
+ }
+
+ directories.push('/usr/share/pixmaps');
+
+ return directories;
+}
+
+function getGtkThemeDirectories(): Promise<DirectoryDescription[]> {
+ // "hicolor" is fallback theme and should always be checked. If no icon is found search is
+ // continued in other themes.
+ const themes = ['hicolor', /.*/];
+ return new Promise((resolve, _reject) => {
+ // Electron modifies XDG_CURRENT_DESKTOP and saves the old value in ORIGINAL_XDG_CURRENT_DESKTOP
+ const xdgCurrentDesktop =
+ process.env.ORIGINAL_XDG_CURRENT_DESKTOP ?? process.env.XDG_CURRENT_DESKTOP ?? '';
+ child_process.exec(
+ 'gsettings get org.gnome.desktop.interface icon-theme',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ { env: { XDG_CURRENT_DESKTOP: xdgCurrentDesktop } },
+ (error, stdout) => {
+ if (error) {
+ log.error('Error while retrieving theme', error);
+ resolve(themes);
+ } else {
+ const theme = stdout.trim().replace(new RegExp("^'|'$", 'g'), '');
+ resolve(theme === '' ? themes : [theme, ...themes]);
+ }
+ },
+ );
+ });
+}
+
+// Searches through a directory tree according to the directory lists supplied. E.g. The arguments
+// ('mullvad', ['svg', 'png'], [['a', 'b'], ['c', 'd']]) will search for mullvad.svg and mullvad.png
+// in the directories a, a/c, a/d, b, b/c and b/d.
+async function findIcon(
+ name: string,
+ extensions: string[],
+ [directories, ...restDirectories]: [string[], ...DirectoryDescription[][]],
+): Promise<string | undefined> {
+ for (const directory of directories) {
+ let contents: string[] | undefined;
+ try {
+ contents = await fs.promises.readdir(directory);
+ } catch (error) {
+ // Non-existent directories and files (not a directory) are expected.
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') {
+ log.error(`Failed to open directory while searching for ${name} icon`, error);
+ }
+ }
+
+ if (contents) {
+ const iconPath = contents.find((item) =>
+ extensions.some((extension) => item === `${name}.${extension}`),
+ );
+
+ if (iconPath) {
+ return path.join(directory, iconPath);
+ } else if (restDirectories.length > 0) {
+ const nextDirectories = matchDirectories(restDirectories[0], contents);
+ const iconPath = await findIcon(name, extensions, [
+ nextDirectories.map((nextDirectory) => path.join(directory, nextDirectory)),
+ ...restDirectories.slice(1),
+ ]);
+
+ if (iconPath) {
+ return iconPath;
+ }
+ }
+ }
+ }
+
+ return undefined;
+}
+
+function matchDirectories(directories: DirectoryDescription[], contents: string[]) {
+ const matches = directories
+ .map((directory) =>
+ directory instanceof RegExp ? contents.filter((item) => directory.test(item)) : directory,
+ )
+ .flat();
+
+ // Remove duplicates
+ return [...new Set(matches)];
+}
diff --git a/gui/src/main/linux-split-tunneling.ts b/gui/src/main/linux-split-tunneling.ts
index 71f9f76eba..0552a41ff9 100644
--- a/gui/src/main/linux-split-tunneling.ts
+++ b/gui/src/main/linux-split-tunneling.ts
@@ -1,11 +1,10 @@
import argvSplit from 'argv-split';
import child_process from 'child_process';
-import log from 'electron-log';
-import fs from 'fs';
import linuxAppList, { AppData } from 'linux-app-list';
import path from 'path';
-import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application';
import { pascalCaseToCamelCase } from './transform-object-keys';
+import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
+import { getAppIcon, shouldShowApplication } from './linux-desktop-entry';
const PROBLEMATIC_APPLICATIONS = {
launchingInExistingProcess: [
@@ -21,19 +20,7 @@ const PROBLEMATIC_APPLICATIONS = {
launchingElsewhere: ['gnome-terminal'],
};
-type DirectoryDescription = string | RegExp;
-
-interface IApplication extends ISplitTunnelingApplication {
- type: string;
- terminal?: string;
- noDisplay?: string;
- hidden?: string;
- onlyShowIn?: string | string[];
- notShowIn?: string | string[];
- tryExec?: string;
-}
-
-export function launchApplication(app: ISplitTunnelingApplication | string) {
+export function launchApplication(app: ILinuxSplitTunnelingApplication | string) {
const excludeArguments = typeof app === 'string' ? [app] : formatExec(app.exec);
child_process.spawn('mullvad-exclude', excludeArguments, { detached: true });
}
@@ -44,55 +31,25 @@ function formatExec(exec: string) {
}
// TODO: Switch to asyncronous reading of .desktop files
-export function getApplications(locale: string): Promise<ISplitTunnelingApplication[]> {
+export function getApplications(locale: string): Promise<ILinuxSplitTunnelingApplication[]> {
const appList = linuxAppList();
const applications = appList
.list()
.map((filename) => {
const applications = localizeNameAndIcon(appList.data(filename), locale);
- return pascalCaseToCamelCase<IApplication>(applications);
+ return pascalCaseToCamelCase<ILinuxSplitTunnelingApplication>(applications);
})
.filter(shouldShowApplication)
.map(addApplicationWarnings)
.sort((a, b) => a.name.localeCompare(b.name))
- .map(async (app) => ({ ...app, icon: await replaceWithIconPath(app.icon) }));
+ .map(async (app) => ({ ...app, icon: await getAppIcon(app.icon) }));
return Promise.all(applications);
}
-// Implemented according to freedesktop specification
-// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
-// TODO: Respect "TryExec"
-function shouldShowApplication(application: IApplication): boolean {
- const originalXdgCurrentDesktop = process.env.ORIGINAL_XDG_CURRENT_DESKTOP?.split(':') ?? [];
- const xdgCurrentDesktop = process.env.XDG_CURRENT_DESKTOP?.split(':') ?? [];
- const desktopEnvironments = originalXdgCurrentDesktop.concat(xdgCurrentDesktop);
-
- const notShowIn =
- typeof application.notShowIn === 'string' ? [application.notShowIn] : application.notShowIn;
- const onlyShowIn =
- typeof application.onlyShowIn === 'string' ? [application.onlyShowIn] : application.onlyShowIn;
-
- const notShowInMatch = notShowIn?.some((desktopEnvironment) =>
- desktopEnvironments?.includes(desktopEnvironment),
- );
- const onlyShowInMatch =
- onlyShowIn?.some((desktopEnvironment) => desktopEnvironments?.includes(desktopEnvironment)) ??
- false;
-
- return (
- application.type === 'Application' &&
- application.name !== 'Mullvad VPN' &&
- application.exec !== undefined &&
- application.noDisplay !== 'true' &&
- application.terminal !== 'true' &&
- application.hidden !== 'true' &&
- !notShowInMatch &&
- (!application.onlyShowIn || onlyShowInMatch)
- );
-}
-
-function addApplicationWarnings(application: IApplication): IApplication {
+function addApplicationWarnings(
+ application: ILinuxSplitTunnelingApplication,
+): ILinuxSplitTunnelingApplication {
const binaryBasename = path.basename(application.exec!.split(' ')[0]);
if (PROBLEMATIC_APPLICATIONS.launchingInExistingProcess.includes(binaryBasename)) {
return {
@@ -122,119 +79,3 @@ function localizeNameAndIcon(application: AppData, locale: string) {
return application;
}
-
-function getGtkThemeDirectories(): Promise<DirectoryDescription[]> {
- // "hicolor" is fallback theme and should always be checked. If no icon is found search is
- // continued in other themes.
- const themes = ['hicolor', /.*/];
- return new Promise((resolve, _reject) => {
- // Electron modifies XDG_CURRENT_DESKTOP and saves the old value in ORIGINAL_XDG_CURRENT_DESKTOP
- const xdgCurrentDesktop =
- process.env.ORIGINAL_XDG_CURRENT_DESKTOP ?? process.env.XDG_CURRENT_DESKTOP ?? '';
- child_process.exec(
- 'gsettings get org.gnome.desktop.interface icon-theme',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- { env: { XDG_CURRENT_DESKTOP: xdgCurrentDesktop } },
- (error, stdout) => {
- if (error) {
- log.error('Error while retrieving theme', error);
- resolve(themes);
- } else {
- const theme = stdout.trim().replace(new RegExp("^'|'$", 'g'), '');
- resolve(theme === '' ? themes : [theme, ...themes]);
- }
- },
- );
- });
-}
-
-// Implemented according to freedesktop specification.
-// https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
-function getIconDirectories() {
- const directories: string[] = [];
-
- if (process.env.HOME) {
- directories.push(path.join(process.env.HOME, '.icons'));
- directories.push(path.join(process.env.HOME, '.local', 'share', 'icons')); // For KDE Plasma
- }
-
- if (process.env.XDG_DATA_DIRS) {
- const dataDirs = process.env.XDG_DATA_DIRS.split(':').map((dir) => path.join(dir, 'icons'));
- directories.push(...dataDirs);
- }
-
- directories.push('/usr/share/pixmaps');
-
- return directories;
-}
-
-async function replaceWithIconPath(name?: string) {
- if (!name || path.isAbsolute(name)) {
- return name;
- }
-
- // Chrome doesn't support .xpm files
- const extensions = ['svg', 'png'];
- return findIcon(name, extensions, [
- getIconDirectories(),
- await getGtkThemeDirectories(),
- // Begin with preferred sized but if nothing matches other sizes should be considered as well.
- ['scalable', '256x256', '512x512', '256x256@2x', '128x128@2x', '128x128', /^\d+x\d+(@2x)?$/],
- // Search in all categories of icons.
- [/.*/],
- ]);
-}
-
-// Searches through a directory tree according to the directory lists supplied. E.g. The arguments
-// ('mullvad', ['svg', 'png'], [['a', 'b'], ['c', 'd']]) will search for mullvad.svg and mullvad.png
-// in the directories a, a/c, a/d, b, b/c and b/d.
-async function findIcon(
- name: string,
- extensions: string[],
- [directories, ...restDirectories]: [string[], ...DirectoryDescription[][]],
-): Promise<string | undefined> {
- for (const directory of directories) {
- let contents: string[] | undefined;
- try {
- contents = await fs.promises.readdir(directory);
- } catch (error) {
- // Non-existent directories and files (not a directory) are expected.
- if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') {
- log.error(`Failed to open directory while searching for ${name} icon`, error);
- }
- }
-
- if (contents) {
- const iconPath = contents.find((item) =>
- extensions.some((extension) => item === `${name}.${extension}`),
- );
-
- if (iconPath) {
- return path.join(directory, iconPath);
- } else if (restDirectories.length > 0) {
- const nextDirectories = matchDirectories(restDirectories[0], contents);
- const iconPath = await findIcon(name, extensions, [
- nextDirectories.map((nextDirectory) => path.join(directory, nextDirectory)),
- ...restDirectories.slice(1),
- ]);
-
- if (iconPath) {
- return iconPath;
- }
- }
- }
- }
-
- return undefined;
-}
-
-function matchDirectories(directories: DirectoryDescription[], contents: string[]) {
- const matches = directories
- .map((directory) =>
- directory instanceof RegExp ? contents.filter((item) => directory.test(item)) : directory,
- )
- .flat();
-
- // Remove duplicates
- return [...new Set(matches)];
-}
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index f9cff36663..73019b1e14 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -20,7 +20,7 @@ import { ICurrentAppVersionInfo } from '../main';
import { loadTranslations, messages, relayLocations } from '../shared/gettext';
import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel';
-import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application';
+import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
import { getRendererLogFile, setupLogging } from '../shared/logging';
import consumePromise from '../shared/promise';
import History from './lib/history';
@@ -416,7 +416,7 @@ export default class AppRenderer {
return IpcRendererEventChannel.splitTunneling.getApplications();
}
- public launchExcludedApplication(application: ISplitTunnelingApplication | string) {
+ public launchExcludedApplication(application: ILinuxSplitTunnelingApplication | string) {
consumePromise(IpcRendererEventChannel.splitTunneling.launchApplication(application));
}
diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
index 145fb5a032..3c07e7eca0 100644
--- a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
@@ -4,7 +4,7 @@ import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
-import ISplitTunnelingApplication from '../../shared/linux-split-tunneling-application';
+import { ILinuxSplitTunnelingApplication } from '../../shared/application-types';
import consumePromise from '../../shared/promise';
import { useAppContext } from '../context';
import * as AppButton from './AppButton';
@@ -104,7 +104,7 @@ export default function LinuxSplitTunnelingSettings() {
} = useAppContext();
const history = useHistory();
- const [applications, setApplications] = useState<ISplitTunnelingApplication[]>();
+ const [applications, setApplications] = useState<ILinuxSplitTunnelingApplication[]>();
const [applicationListHeight, setApplicationListHeight] = useState<number>();
const [browsing, setBrowsing] = useState(false);
@@ -202,8 +202,8 @@ export default function LinuxSplitTunnelingSettings() {
}
interface IApplicationRowProps {
- application: ISplitTunnelingApplication;
- launchApplication: (application: ISplitTunnelingApplication) => void;
+ application: ILinuxSplitTunnelingApplication;
+ launchApplication: (application: ILinuxSplitTunnelingApplication) => void;
}
function ApplicationRow(props: IApplicationRowProps) {
diff --git a/gui/src/shared/application-types.ts b/gui/src/shared/application-types.ts
new file mode 100644
index 0000000000..91b8068072
--- /dev/null
+++ b/gui/src/shared/application-types.ts
@@ -0,0 +1,22 @@
+type Warning = 'launches-in-existing-process' | 'launches-elsewhere';
+
+export interface IApplication {
+ absolutepath: string;
+ name: string;
+ icon?: string;
+}
+
+export interface ILinuxApplication extends IApplication {
+ exec: string;
+ type: string;
+ terminal?: string;
+ noDisplay?: string;
+ hidden?: string;
+ onlyShowIn?: string | string[];
+ notShowIn?: string | string[];
+ tryExec?: string;
+}
+
+export interface ILinuxSplitTunnelingApplication extends ILinuxApplication {
+ warning?: Warning;
+}
diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts
index 1524e24bba..58186a822d 100644
--- a/gui/src/shared/ipc-event-channel.ts
+++ b/gui/src/shared/ipc-event-channel.ts
@@ -1,6 +1,6 @@
import { ICurrentAppVersionInfo } from '../main/index';
import { IWindowShapeParameters } from '../main/window-controller';
-import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application';
+import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
import {
AccountToken,
BridgeSettings,
@@ -175,8 +175,8 @@ const ipc = {
verifyKey: invoke<void, boolean>(),
},
splitTunneling: {
- getApplications: invoke<void, ISplitTunnelingApplication[]>(),
- launchApplication: invoke<ISplitTunnelingApplication | string, void>(),
+ getApplications: invoke<void, ILinuxSplitTunnelingApplication[]>(),
+ launchApplication: invoke<ILinuxSplitTunnelingApplication | string, void>(),
},
problemReport: {
collectLogs: invoke<string[], string>(),
diff --git a/gui/src/shared/linux-split-tunneling-application.ts b/gui/src/shared/linux-split-tunneling-application.ts
deleted file mode 100644
index a96152b70d..0000000000
--- a/gui/src/shared/linux-split-tunneling-application.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-type Warning = 'launches-in-existing-process' | 'launches-elsewhere';
-
-export default interface ISplitTunnelingApplication {
- absolutepath: string;
- name: string;
- exec: string;
- icon?: string;
- warning?: Warning;
-}