Mailspring/app/src/linux-theme-utils.ts

317 lines
8 KiB
TypeScript

import temp from 'temp';
import os from 'os';
import path from 'path';
import fs from 'fs';
import { execSync } from 'child_process';
import ini from 'ini';
const Context = {
ACTIONS: 'actions',
ANIMATIONS: 'animations',
APPLICATIONS: 'applications',
CATEGORIES: 'categories',
DEVICES: 'devices',
EMBLEMS: 'emblems',
EMOTES: 'emotes',
INTERNATIONAL: 'international',
MIMETYPES: 'mimetypes',
PANEL: 'panel',
PLACES: 'places',
STATUS: 'status',
};
const HOME = os.homedir();
const ICON_THEME_PATHS = [
path.join(HOME, '.local/share/icons'),
path.join(HOME, '.icons'),
'/usr/share/icons',
'/usr/local/share/icons',
];
const DESKTOP = process.env.XDG_CURRENT_DESKTOP;
const ICON_EXTENSION = ['.png', '.svg'];
/**
* Create a gsettings command string for icons themes
*
* @returns {string} command
* @private
*/
function __getGsettingsIconThemeCMD(): string {
const path = __getDesktopSettingsPath();
const key = 'icon-theme';
return path != null ? `gsettings get ${path} ${key}` : null;
}
/**
* Create a gsettings command string for themes
*
* @returns {string} command
* @private
*/
function __getGsettingsGtkThemeCMD(): string {
const path = __getDesktopSettingsPath();
const key = 'gtk-theme';
return path != null ? `gsettings ${path} ${key}` : null;
}
/**
* Returns the path for the gsettings command. Currently only GNOME and MATE are supported
*
* @returns {*}
* @private
*/
function __getDesktopSettingsPath(): string {
switch (DESKTOP) {
case 'MATE':
return 'org.mate.interface';
case 'GNOME':
case 'Budgie:GNOME':
return 'org.gnome.desktop.interface';
default:
return null;
}
}
/**
* Execute a command and return the string result
*
* @param {string} cmd to execute
* @returns {string} result of the command
* @private
*/
function __exec(cmd: string): string {
try {
return cmd == null
? null
: execSync(cmd)
.toString()
.trim()
.replace(/'/g, '');
} catch (error) {
console.warn(error);
return null;
}
}
/**
* Get the current icon theme
*
* @returns {string} name of the icon
*/
function getIconThemeName(): string {
return __exec(__getGsettingsIconThemeCMD());
}
/**
* Get the current GTK theme
*
* @returns {string} name of the theme
*/
function getThemeName(): string {
return __exec(__getGsettingsGtkThemeCMD());
}
/**
* Parse the icon themes index.theme file
*
* @param {string} themePath path to the themes root
* @returns {object} parsed ini file
* @private
*/
function __parseIconTheme(themePath: string): object {
const themeIndex = path.join(themePath, 'index.theme');
if (fs.existsSync(themeIndex)) {
return ini.parse(fs.readFileSync(themeIndex, 'utf-8'));
}
return null;
}
/**
* Find the path to the icon theme and parse it
*
* @param {string} themeName to parse
* @returns {IconTheme} containing the parsed index.theme file and path to the theme
*/
function getIconTheme(themeName): IconTheme {
if (themeName != null) {
for (const themesPath of ICON_THEME_PATHS) {
const themePath = path.join(themesPath, themeName);
const parsed = __parseIconTheme(themePath);
if (parsed != null) {
return {
themePath: themePath,
data: parsed,
};
}
}
}
return null;
}
type IconTheme = {
themePath: string;
data: object;
};
/**
* Get all possible icon paths
*
* @param {IconTheme} theme data
* @param {string} iconName name of the icon
* @param {number} size of the icon
* @param {string|string[]} contexts of the icon
* @param {1|2} [scale=1] of the icon
* @returns {string[]} with all possibilities of the icon in one theme
* @private
*/
function __getAllIconPaths(
theme: IconTheme,
iconName: string,
size: number,
contexts: string | string[],
scale: 1 | 2 = 1
): string[] {
const icons = [];
if (!(contexts instanceof Array)) {
contexts = [contexts];
}
for (const [sectionName, section] of Object.entries(theme.data)) {
if (sectionName !== 'Icon Theme') {
const _context = (section.Context || '').toLowerCase();
const _size = parseInt(section.Size, 10);
const _minSize = parseInt(section.MinSize || section.Size, 10);
const _maxSize = parseInt(section.MaxSize || section.Size, 10);
const _scale = parseInt(section.Scale || 1, 10);
if (
contexts.indexOf(_context) > -1 &&
(_size === size || (_minSize <= size && size <= _maxSize)) &&
(scale == null || scale === _scale)
) {
const iconDir = path.join(theme.themePath, sectionName);
for (const extension of ICON_EXTENSION) {
const iconPath = path.join(iconDir, iconName + extension);
if (fs.existsSync(iconPath)) {
icons.push(iconPath);
}
}
}
}
}
return icons;
}
/**
* Get the icon from a theme
*
* @param {IconTheme} theme name
* @param {string} iconName name of the icon
* @param {number} size of the icon
* @param {string|string[]} contexts to search in
* @param {1|2} [scale=1] of the icon
* @returns {string} path to the icon or null
*/
function getIconFromTheme(
theme: IconTheme,
iconName: string,
size: number,
contexts: string | string[],
scale: 1 | 2 = 1
) {
const icons = __getAllIconPaths(theme, iconName, size, contexts, scale);
for (let path of icons) {
if (fs.existsSync(path)) {
return fs.realpathSync(path);
}
}
return null;
}
/**
* Get the absolute path to the icon. If the icon is not found in the theme itself, the inherited
* themes will be searched
*
* @param {string} iconName to search for
* @param {number} size of the icon
* @param {array|string} context the icons context
* @param {1|2} [scale=1] 1 = normal, 2 = HiDPI version
* @returns {string} absolute path of the icon
*/
function getIconPath(iconName: string, size: number, context: string | string[], scale: 1 | 2 = 1) {
let defaultTheme = getIconTheme(getIconThemeName());
if (defaultTheme != null) {
let inherits = defaultTheme.data['Icon Theme']['Inherits'].split(',');
let icon = getIconFromTheme(defaultTheme, iconName, size, context, scale);
if (icon !== null) {
return icon;
}
// in case the icon was not found in the theme, we search the inherited themes
for (let key of inherits) {
let inheritsTheme = getIconTheme(inherits[key]);
icon = getIconFromTheme(inheritsTheme, iconName, size, context);
if (icon !== null) {
return icon;
}
}
}
return null;
}
/**
* Return an icon from the current icon theme
*
* @param {string} iconName name of the icon you want to search for (i.e. mailspring)
* @param {number} [size=22] size of the icon, if no exact size is found, the next possible one will be chosen
* @param {array|string} [context=Context.APPLICATIONS] icon context to search in, defaults to APPLICATIONS
* @param {number} [scale=2] icon scale, defaults to HiDPI version
* @returns {string} path to the icon
*/
function getIcon(
iconName,
size = 22,
context: string | string[] = [Context.APPLICATIONS],
scale: 1 | 2 = 2
) {
if (process.platform !== 'linux') {
throw Error('getIcon only works on linux');
}
return getIconPath(iconName, size, context, scale);
}
/**
* Convert any icon to a png using ImageMagick. If ImageMagick is not present the icon cannot be
* converted.
*
* @param {string} iconName to name the tmp file
* @param {string} iconPath to the original icon to be converted
* @returns {string} path to the converted tmp file
*/
function convertToPNG(iconName: string, iconPath: string) {
try {
const version = execSync('convert --version')
.toString()
.trim();
if (!version) {
console.warn('Cannot find ImageMagick');
return null;
}
const tmpFile = temp.openSync({ prefix: iconName, suffix: '.png' });
execSync(`convert ${iconPath} -transparent white ${tmpFile.path}`);
return tmpFile.path;
} catch (error) {
console.warn(error);
}
return null;
}
export { convertToPNG, getIcon, getIconThemeName, getThemeName, Context };