alfy
Version:
Create Alfred workflows with ease
200 lines (157 loc) • 4.65 kB
JavaScript
import os from 'node:os';
import process from 'node:process';
import {createRequire} from 'node:module';
import Conf from 'conf';
import got from 'got';
import loudRejection from 'loud-rejection';
import cleanStack from 'clean-stack';
import {getProperty} from 'dot-prop';
import AlfredConfig from 'alfred-config';
import updateNotification from './lib/update-notification.js'; // eslint-disable-line import-x/order
const require = createRequire(import.meta.url);
const CacheConf = require('cache-conf');
const alfy = {};
// Track if output has been generated to avoid empty JSON
let hasOutput = false;
updateNotification();
const getIcon = name => `/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/${name}.icns`;
const getEnv = key => process.env[`alfred_${key}`];
alfy.meta = {
name: getEnv('workflow_name'),
version: getEnv('workflow_version'),
uid: getEnv('workflow_uid'),
bundleId: getEnv('workflow_bundleid'),
};
alfy.alfred = {
version: getEnv('version'),
theme: getEnv('theme'),
themeBackground: getEnv('theme_background'),
themeSelectionBackground: getEnv('theme_selection_background'),
themeSubtext: Number(getEnv('theme_subtext')),
data: getEnv('workflow_data'),
cache: getEnv('workflow_cache'),
preferences: getEnv('preferences'),
preferencesLocalHash: getEnv('preferences_localhash'),
};
alfy.input = process.argv[2];
alfy.output = (items, {rerunInterval} = {}) => {
hasOutput = true;
console.log(JSON.stringify({items, rerun: rerunInterval}, null, '\t'));
};
alfy.matches = (input, list, item) => {
input = input.toLowerCase().normalize();
return list.filter(listItem => {
if (typeof item === 'string') {
listItem = getProperty(listItem, item);
}
if (typeof listItem === 'string') {
listItem = listItem.toLowerCase().normalize();
}
if (typeof item === 'function') {
return item(listItem, input);
}
return listItem.includes(input);
});
};
alfy.inputMatches = (list, item) => alfy.matches(alfy.input, list, item);
alfy.log = text => {
console.error(text);
};
alfy.error = error => {
const stack = cleanStack(error.stack || error);
const title = error.stack ? `${error.name}: ${error.message}` : String(error);
const copy = `
\`\`\`
${stack}
\`\`\`
-
${alfy.meta.name} ${alfy.meta.version}
Alfred ${alfy.alfred.version}
${process.platform} ${os.release()}
`.trim();
alfy.output([{
title,
subtitle: 'Press ⌘L to see the full error and ⌘C to copy it.',
valid: false,
text: {
copy,
largetype: stack,
},
icon: {
path: alfy.icon.error,
},
}]);
// Also output to stderr for the debugger (as requested in issue #86)
console.error(stack);
};
alfy.config = new Conf({
projectName: 'alfy',
cwd: alfy.alfred.data,
});
alfy.userConfig = new AlfredConfig();
alfy.cache = new CacheConf({
configName: 'cache',
cwd: alfy.alfred.cache,
version: alfy.meta.version,
});
alfy.fetch = async (url, options) => {
options = {
resolveBodyOnly: true,
...options,
};
if (typeof url !== 'string') {
throw new TypeError(`Expected \`url\` to be a \`string\`, got \`${typeof url}\``);
}
if (options.transform && typeof options.transform !== 'function') {
throw new TypeError(`Expected \`transform\` to be a \`function\`, got \`${typeof options.transform}\``);
}
const rawKey = url + JSON.stringify(options);
// This must be below the cache key generation.
const {transform, maxAge} = options;
delete options.transform;
delete options.maxAge;
const key = rawKey.replaceAll('.', String.raw`\.`);
const cachedResponse = alfy.cache.get(key, {ignoreMaxAge: true});
if (cachedResponse && !alfy.cache.isExpired(key)) {
return cachedResponse;
}
if ('json' in options && options.json === false) {
delete options.json;
options.responseType = 'text';
} else {
options.responseType = 'json';
}
let response;
try {
response = await got(url, options);
} catch (error) {
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
const data = transform ? transform(response) : response;
if (maxAge) {
alfy.cache.set(key, data, {maxAge});
}
return data;
};
alfy.debug = getEnv('debug') === '1';
alfy.icon = {
get: getIcon,
info: getIcon('ToolbarInfo'),
warning: getIcon('AlertCautionIcon'),
error: getIcon('AlertStopIcon'),
alert: getIcon('Actions'),
like: getIcon('ToolbarFavoritesIcon'),
delete: getIcon('ToolbarDeleteIcon'),
};
loudRejection(alfy.error);
process.on('uncaughtException', alfy.error);
// Ensure valid JSON output even if user forgets to call alfy.output() (fixes #82)
process.on('beforeExit', () => {
if (!hasOutput) {
alfy.output([]);
}
});
export default alfy;