Skip to content

Commit

Permalink
Use mdfind command to get all Applications + refactoring initializers
Browse files Browse the repository at this point in the history
Now plugins could have two fields: initialize and initializeAsync. First will be called in the same process, and initializeAsync will be called in background window using RPC (like in contacts plugin)

Also apps list now pre-fetched after app start
  • Loading branch information
KELiON committed Nov 17, 2016
1 parent dfb0878 commit 3b9b7aa
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 47 deletions.
2 changes: 0 additions & 2 deletions background/rpc/initialize.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import register from 'lib/rpc/register'

import getAppsList from 'lib/getAppsList'
import getFileDetails from 'lib/getFileDetails'
import getFileSize from 'lib/getFileSize'
import getFileIcon from 'lib/getFileIcon'
Expand All @@ -14,7 +13,6 @@ import initializePlugins from 'lib/initializePlugins'
* After `register` function can be called using rpc from main window
*/
export default () => {
register('getAppsList', getAppsList)
register('getFileDetails', getFileDetails)
register('getFileSize', getFileSize)
register('getFileIcon', getFileIcon)
Expand Down
62 changes: 33 additions & 29 deletions lib/getAppsList.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
import glob from 'glob';
import flatten from 'lodash/flatten';
import mdfind from 'lib/mdfind';

// Patters that we use for searching files
const PATTERNS = [
// Apps in root applications folder
'/Applications/*.app',
// Apps inside other apps
'/Applications/*.app/Contents/Applications/*.app',
// Apps in folders
'/Applications/!(*.app)/**.app',
// System preferences
'/System/Library/PreferencePanes/*.prefPane',

/**
* List of supported files
* @type {Array}
*/
const supportedTypes = [
'com.apple.application-bundle',
'com.apple.systempreference.prefpane'
];

/**
* Promise-wrapper for glob function
* @param {String} pattern Pattern for glob function
* @param {Object} options
* @return {Promise}
* Build mdfind query
*
* @return {String}
*/
const globPromise = (pattern, options) => new Promise((resolve, reject) => {
glob(pattern, options, (err, files) => {
if (err) return reject(err);
resolve(files);
});
});
const buildQuery = () => {
return supportedTypes.map(type => `kMDItemContentType=${type}`).join('||');
}

/**
* Function to terminate previous query
*
* @return {Function}
*/
let cancelPrevious = () => {};

/**
* Get list of all installed applications
* @return {Promise<Array>}
*/
export default function () {
return Promise.all(PATTERNS.map(pattern => globPromise(pattern)))
.then(files => {
return flatten(files).map(path => ({
path,
name: path.match(/\/([^\/]+)\.(app|prefPane)$/i)[1],
}));
export default (term) => {
console.log('Get new apps list')
cancelPrevious();
return new Promise((resolve, reject) => {
const { output, terminate } = mdfind({
query: buildQuery()
});
cancelPrevious = terminate;
const result = [];
output.on('data', (file) => result.push(file));
output.on('end', () => resolve(result));
});
}
6 changes: 3 additions & 3 deletions lib/initializePlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { send } from 'lib/rpc/events';
export default () => {
// Run plugin initializers only when main window is loaded
Object.keys(plugins).forEach(name => {
const { initialize } = plugins[name];
if (!initialize) return;
initialize(data => {
const { initializeAsync } = plugins[name];
if (!initializeAsync) return;
initializeAsync(data => {
// Send message back to main window with initialization result
send('plugin.message', {
name,
Expand Down
79 changes: 79 additions & 0 deletions lib/mdfind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import _ from 'lodash';
import { spawn } from 'child_process';
import { map, split, through } from 'event-stream';

const REAL_KEYS = {
'kMDItemDisplayName': 'name',
'kMDItemLastUsedDate': 'lastUsed'
}

/**
* Parse mdfind result line to JS object
*
* @param {String} line
* @return {Object}
*/
function parseLine(line) {
const attrs = line.split(' ');
const result = {
// First attr is always full path to the item
path: attrs.shift()
}
attrs.forEach(attr => {
const [key, value] = attr.split(' = ');
result[REAL_KEYS[key] || key] = getValue(value);
});
this.emit('data', result);
}

const getValue = (item) => {
if (item === '(null)') {
return null;
} else if (_.startsWith(item, '(\n "') && _.endsWith(item, '"\n)')) {
const actual = item.slice(7, -3);
const lines = actual.split('",\n "');
return lines;
} else {
return item;
}
}

const filterEmpty = (data, done) => {
if (data === '') {
done();
} else {
done(null, data);
}
}

const makeArgs = (array, argName) => {
return _.flatten(array.map(item => [argName, item]));
}

export default function mdfind ({query, attributes = ['kMDItemDisplayName', 'kMDItemLastUsedDate'], names = [], directories = [], live = false, interpret = false, limit} = {}) {
const dirArgs = makeArgs(directories, '-onlyin')
const nameArgs = makeArgs(names, '-name')
const attrArgs = makeArgs(attributes, '-attr')
const interpretArgs = interpret ? ['-interpret'] : []
const queryArgs = query ? [query] : []

const args = ['-0'].concat(dirArgs, nameArgs, attrArgs, interpretArgs, live ? ['-live', '-reprint'] : [], queryArgs)

const child = spawn('mdfind', args)

let times = 0

return {
output: child.stdout
.pipe(split('\0'))
.pipe(map(filterEmpty))
.pipe(through(function (data) {
times++
if (limit && times === limit) child.kill()
if (limit && times > limit) return
this.queue(data)
}))
.pipe(through(parseLine)),
terminate: () => child.kill()
}
}
2 changes: 0 additions & 2 deletions lib/rpc/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ export const listArchive = memoize(wrap('listArchive'), MEMOIZE_OPTIONS)
export const getFileIcon = memoize(wrap('getFileIcon'), MEMOIZE_OPTIONS)
export const readDir = memoize(wrap('readDir'), MEMOIZE_OPTIONS)

export const getAppsList = memoize(wrap('getAppsList'), MEMOIZE_OPTIONS);

export { default as initializePlugins } from './initializePlugins';
16 changes: 15 additions & 1 deletion lib/rpc/functions/initializePlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@ import wrap from '../wrap';
import * as plugins from 'main/plugins/';
import { on } from 'lib/rpc/events';

const initialize = wrap('initializePlugins');
const asyncInitializePlugins = wrap('initializePlugins');

const initialize = () => {
// Call background initializers
asyncInitializePlugins();

// Call foreground initializers
Object.keys(plugins).forEach(name => {
const { initialize } = plugins[name];
if (initialize) {
// Sync plugin initialization
initialize();
}
});
}

/**
* RPC-call for plugins initializations
Expand Down
38 changes: 30 additions & 8 deletions main/plugins/apps/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import React from 'react';

import fs from 'fs';
import { getAppsList } from 'lib/rpc/functions';
import getAppsList from 'lib/getAppsList';
import search from 'lib/search';
import shellCommand from 'lib/shellCommand';
import Preview from './Preview';
import { shell } from 'electron';
import memoize from 'memoizee';

/**
* Time for apps list cache
* @type {Integer}
*/
const CACHE_TIME = 2 * 60 * 1000;

/**
* Cache getAppsList function
* @type {Function}
*/
const cachedAppsList = memoize(getAppsList, {
length: false,
promise: 'then',
maxAge: CACHE_TIME,
preFetch: true
});

const appsPlugin = (term, callback) => {
getAppsList().then(items => {
cachedAppsList().then(items => {
const result = search(items, term, (file) => file.name).map(file => {
const { path, name } = file;
const shellPath = path.replace(/ /g, '\\ ');
return {
title: name,
term: name,
Expand All @@ -25,7 +39,7 @@ const appsPlugin = (term, callback) => {
event.preventDefault();
}
},
onSelect: () => shellCommand(`open ${shellPath}`),
onSelect: () => shell.openItem(path),
getPreview: () => <Preview name={name} path={path} />
};
});
Expand All @@ -34,5 +48,13 @@ const appsPlugin = (term, callback) => {
};

export default {
fn: appsPlugin
fn: appsPlugin,
initialize: () => {
// Cache apps cache and force cache reloading in background
const recache = () => {
cachedAppsList();
setTimeout(recache, CACHE_TIME * 0.75);
}
recache();
}
};
4 changes: 2 additions & 2 deletions main/plugins/contacts/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import Preview from './Preview';
import search from 'lib/search';
import initialize from './initialize';
import initializeAsync from './initialize';

/**
* List of all contacts from osx address book
Expand Down Expand Up @@ -38,7 +38,7 @@ const contactsPlugin = (term, callback) => {
};

export default {
initialize,
initializeAsync,
name: 'Contacts',
fn: contactsPlugin,
onMessage: (contacts) => {
Expand Down

0 comments on commit 3b9b7aa

Please sign in to comment.