UNPKG

file-icon

Version:

Get the icon of a file or app as a PNG image (macOS)

96 lines (71 loc) 3.07 kB
import process from 'node:process'; import path from 'node:path'; import {promisify} from 'node:util'; import {fileURLToPath} from 'node:url'; import {execFile} from 'node:child_process'; import pMap from 'p-map'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const execFileP = promisify(execFile); const binary = path.join(__dirname, 'file-icon'); const HUNDRED_MEGABYTES = 1024 * 1024 * 100; const spawnOptions = { encoding: null, maxBuffer: HUNDRED_MEGABYTES, }; const validate = (file, options) => { options = { size: 1024, ...options, }; if (process.platform !== 'darwin') { throw new Error('macOS only'); } if (!file) { throw new Error('Specify one or an array of: app name, bundle identifier, file path, or pid'); } if (typeof file !== 'string' && typeof file !== 'number' && !Array.isArray(file)) { throw new TypeError(`Expected \`file\` be a string, number, or an array, got \`${typeof file}\``); } if (Array.isArray(file) && !file.every(f => typeof f === 'string' || typeof f === 'number')) { throw new TypeError('Expected all members of `file` array to be of `string` or `number` type'); } if (typeof options.size !== 'number') { options.size = 1024; } if (options.size > 1024) { throw new Error('Size must be 1024 or less'); } return options; }; const toArray = input => Array.isArray(input) ? input : [input]; const toCLIArgument = (file, {size, destination}) => { const toBuffer = file => ({appOrPID: file.toString(), size}); const toFile = (file, index) => ({...toBuffer(file), destination: toArray(destination)[index]}); // eslint-disable-next-line unicorn/no-array-callback-reference const argument_ = toArray(file).map(destination ? toFile : toBuffer); return JSON.stringify(argument_); }; export async function fileIconToBuffer(file, options) { options = validate(file, options); const files = toArray(file); const mapper = async file => { const {stdout} = await execFileP(binary, [toCLIArgument(file, options)], spawnOptions); return stdout; }; const buffers = await pMap(files, mapper, {concurrency: 8}); return buffers.length === 1 && !Array.isArray(file) ? buffers[0] : buffers; } export async function fileIconToFile(file, options) { options = validate(file, options); const isArray = Array.isArray(file); if ((typeof file === 'string' || typeof file === 'number') && typeof options.destination !== 'string') { throw new TypeError(`Expected \`options.destination\` to be of type \`string\` when \`file\` is of type \`string\` or \`number\`, got \`${typeof options.destination}\``); } else if (isArray && !Array.isArray(options.destination)) { throw new TypeError(`Expected \`options.destination\` to be of type \`array\` when \`file\` is of type \`array\`, got \`${typeof options.destination}\``); } else if (isArray && file.length !== options.destination.length) { throw new TypeError('Expected `file` and `options.destination` arrays to be of the same length'); } await execFileP(binary, [toCLIArgument(file, options)], spawnOptions); }