Alfred Utilities

Posted Sunday, March 13, 2022 by Sri.Tagged TOOL
EDIT STATUS:new

Alfred is pop-up command line interface for Mac that makes it much faster to access the apps and files you need. At its heart it works a lot like Apple's Spotlight Search, but users can extend the functionality with "workflow" scripts written in AppleScript or Javascript for Automation.

I have a "Sri Utils" workflow that allows me to type go projectname and it will open up all the apps, websites, and file folders for it and speak the name. I chose to write it in Javascript for Automation, though the tools and documentation are very poor.

Here's the current version, edited to remove personal information.

/*////////////////////////// ALFRED OPEN FOLDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\*\

A script to accept an input query keyword, then open a set of folders/files
via the finder. I made this to quickly open folders and related files by
keyword. CMD-SPACE followed by cd km, fo example, opens my ~/KM folder and
launches a VSCODE project after speaking a custom prompt.

/// CONSTANTS ///////////////////////////////////////////////////////////////*/

/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// define constants for long paths
const DESKTOP = `~/Desktop`;
const DROPBOX = `${DESKTOP}/Dropbox`;
const GDRIVE = `${DESKTOP}/Google\ Drive`;
const KMBASE = `${DROPBOX}/Working Docs/KM`;
const CLIENTS = `${DROPBOX}/@CLIENTS`;
const PERSONAL = `${DROPBOX}/@SRI-PROJECTS`;
const COMMERCE = `${DROPBOX}/@SRI-COMMERCE`;
const TIMELOG = `${DROPBOX}/@SRI-OFFICE/Path/`;
const INQDOCS = `${DROPBOX}/Path/Path/`
const DSCOM = `${DROPBOX}/@SRI-PROJECTS/Path/`;
const DSRICOM = `${DROPBOX}/@SRI-PROJECTS/Path/`;
const ORGS = `${DROPBOX}/@ORGS`;
const VHOSTS = '~/Web/vhosts';
const BIZ = `${DROPBOX}/@SRI-OFFICE`;
const TYPORA = `/Applications/Typora.app`;
const SRCTREE = `/Applications/Sourcetree.app`;
const LROOM = `/Applications/Adobe\ Lightroom\ Classic/Adobe\ Lightroom\ Classic.app`;
const EXCEL = `/Applications/Microsoft Excel.app`;
const DEVGEM = `~/Dev/Path`;

// for shell commands: use quotedForms to wrap quoted arguments, but not pure
// shell commands.
function quotedForm(s) { return "'" + s.replace(/'/g, "'\\''") + "'" }

/** ENTRY SYNTAX *************************************************************\

ENTRY KEYS: parameters for script
ENTRY VALUES:
string - single action definition
array - multiple action definitions

available actions:
string 'path' - what to open (can be dir, document, or app!)
string 'say: phrase' - use voice synthesizer to say phrase
string 'shell: cmds' - execute a shell command
string 'flag: nofocus' - tell finder not to highlight opened item
array [ 'fileOrDir', 'pathToApp' ] - open fileOrDir with a specific app

\*****************************************************************************/

const ENTRIES = {
user : '~/',
me : [
[
`${KMBASE}/path/doc.md`,
`${TYPORA}`
],
`/Applications/Notion.app`,
'say: continuity'
],
desk : DESKTOP,
drop : DROPBOX,
clients : CLIENTS,
dev : '~/Dev',
web : '~/Web',
dslr : [
'say: opening dee ess el are tools',
`${LROOM}`,
`shell: if [ ! -d /Volumes/MountPoint ]; then /usr/bin/open "smb://server/MountPoint"; fi`
],
dscom : [
`${DSCOM}`,
`${VHOSTS}/site/vscode-workspace`,
`/Applications/MAMP PRO.app`,
'say: opening DEE ESS COM'
],
ecomm : [
`${COMMERCE}/`,
'say: getting e-commerce folders'
],
xyzzy : [
`${COMMERCE}/Shops/StoreName/`,
'say: opening StoreName store data'
],
shipping : [
`${COMMERCE}/shops/StoreName`,
'say: opening StoreName shipping data'
],
images : `${DSCOM}/httpdocs-synched/_wpcontent/images/20`,
km : [
`${KMBASE}`,
`${KMBASE}/ProjectName.code-workspace`,
// special string to override default speech prompt
'say: getting KM-SSG'
],
dsri : [
`${DSRICOM}/ProjectName`,
`${DSRICOM}/ProjectName/ProjectName.code-workspace`,
'url: hostname:8080',
'say: getting deesree SSG'
],
freelance : [
`${BIZ}/AccountingPath/Spreadsheet.xlsx`,
'say: getting timesheet',
'flag: nofocus'
],
invoice : [
`${BIZ}/AccountingPath/Spreadsheet.xlsx`,
'say: getting invoice',
'flag: nofocus'
],
gem : [
'~/Dev/ProjectName.code-workspace',
SRCTREE,
[
`${INQDOCS}/ProjectName/DocumentIndex.md`,
`${TYPORA}`
],
[
`${TIMELOG}/SpreadSheet.xlsx`,
`${EXCEL}`
],
'url: localhost',
'say: opening ProjectName'
],
meme : [
'~/Dev/ProjectName.code-workspace',
'say: opening meem'
],
nc : [
'~/Dev/ProjectName.code-workspace',
'say: opening ProjectName'
],
obs : [
'/Applications/OBS.app',
'url: www.youtube.com/live_dashboard',
'url: www.youtube.com/live_chat?v=room_key',
'say: opening OH BEE ESS'
]

} // end ENTRIES


/// MODULE LEVEL VARS /////////////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// m_app refers to this running script!
let m_app = Application.currentApplication();
m_app.includeStandardAdditions = true;

let m_say;
let m_finder = Application("Finder");
let m_focus = m_finder;
let m_chrome;

/// ENTRY POINT ///////////////////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*/ alfred loads this script and expects there to be a run() function to call
with the 'query' received from the input.
The run() function should return data for the next linked item to use but
THIS script doesn't return anything useful since all the action happens
here
/*/
function run(argv) {
// try to find chrome instance
try {
m_chrome = Application("Google Chrome");
} catch (e) {
console.log('error',e);
}

let query = argv[0].trim();
console.log(`contents of argv`,JSON.stringify(argv));

// does the query exist in our ENTRIES table?
// if not, throw up an alert window and exit
if (!ENTRIES.hasOwnProperty(query)) {
m_app.displayAlert(`I don't have a path alias for '${query}'`);
return query;
}

// if we got this far, we have a valid keyword

let entry = ENTRIES[query];
let opens = [];

// the opens[] array is loaded with string paths to open via Finder
// check for special case string 'say:' to override voice prompt
if (Array.isArray(entry)) for (entryItem of entry) {
if (DidProcessFlag(entryItem)) continue;
else opens.push(entryItem);
} else {
opens.push(entry);
}

let win, tab, tabIndex;
let firstTab;

// done processing, so use MacOS speech synthesis
m_app.say(m_say || `getting ${query}`);
// get an instance of the Finder, and tell it to open
// each path in opens[] array. the strings in opens[]
// have to be converted to the Path data structure.
opens.forEach( element => {
ProcessElement(element);
});

// tell the Finder to activate in case it's behind anything
// not sure this is necessary
if (m_focus) m_focus.activate();
if (firstTab) {
win.activeTabIndex.set(firstTab);
}

} // end function run()


/// SUPPORT FUNCTIONS /////////////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*/ DidProcessFlag() accepts a string, looking for keywords at the beginning
to set other global flags that affect how the workflow behaves.
Returns TRUE if it processed a flag, FALSE if there was no flag
/*/
function DidProcessFlag( entryItem ) {
if (typeof entryItem!=='string') return false;

if (entryItem.startsWith('say:')) {
m_say = entryItem.substring(4).trim();
return true;
}

if (entryItem.startsWith('shell:')) {
let cmd = entryItem.substring(6).trim();
console.log('shell', m_app.doShellScript(cmd));
return true;
}

if (entryItem.startsWith('flag:')) {
let flag = entryItem.substring(5).trim();
switch (flag) {
case 'nofocus' : m_focus = null; break;
default:
m_app.displayAlert(`error processing flag:${flag}`)
}
return true;
}

return false;
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*/ ProcessElement() accepts an entry object, and processes it accordingly
/*/
function ProcessElement(element) {
// open element with specific application
if (Array.isArray(element)) {
if (element.length!==2) {
console.log('open with requires two paths');
return;
}
console.log(`opening ${element[0]}`);
m_finder.open(
Path($(element[0]).stringByStandardizingPath.js),
{
'using': Path($(element[1]).stringByStandardizingPath.js),
}
);
return;
}

// mount drives
if (element.startsWith('mount')) {
m_finder.mount
}

// open urls
if (element.startsWith('url:')) {
if (!m_chrome) {
m_app.displayAlert(`Couldn't find Chrome...skipping url opening`);
return;
}

let url = element.substring(4).trim();
win = m_chrome.windows[0]; // use existing window
if (!win) {
win = m_chrome.Window().make(); // make new window
tab = win.tabs[0];
} else {
win.tabs.push(tab = m_chrome.Tab());
}
if (!url.startsWith('localhost')) tab.url.set(`https://${url}`);
else tab.url.set(`http://${url}`);
// if (!firstTab) firstTab = win.tabs.length;
return;
}

// open document by default
m_finder.open( Path($(element).stringByStandardizingPath.js) );
}