A TaskPaper 3 plain text outline can contain a number of custom perspectives or 'saved searches'
Here is a macro for gathering existing searches into a menu, so that you can add some subset of them to a new TaskPaper 3 document. The macro gathers searches into the menu from:
- All open TaskPaper 3 documents, and
- any default TaskPaper 3 documents that are listed in the macro (whether they are open or not)
Add existing TaskPaper 3 searches to new document.kmmacros (28.5 KB)
Source of the JavaScript for Automation action:
// SEARCH MENU WHICH DRAWS ITS ITEMS FROM ALL OPEN TASKPAPER 3 DOCS,
// AND OPTIONALLY ALSO FROM DEFAULT TASKPAPER 3 DOCS, WHICH CAN BE CLOSED
//
// Offers and adds saved searches
//
// FROM: 1. All open TaskPaper 3 documents
// 2. One or more default TaskPaper documents
// (give path(s) in options at bottom of script)
// which don't need to be open.
//
// TO: The active (front) TaskPaper 3 document
// NB
// Adjust the options at the bottom of the script before use
//
// Rob Trew ver .015
(function (dctOptions) {
'use strict';
// TASKPAPER CONTEXT
// Editor -> [String]
function fnFindSearches(e) {
return e.outline.evaluateItemPath('//@search')
.map(function (x) {
return x.bodyString;
});
}
// Editor -> Options -> () -> String
function fnUseSearches(editor, options) {
// concatMap :: (a -> [b]) -> [a] -> [b]
function concatMap(f, xs) {
return [].concat.apply([], xs.map(f));
}
// searchLine :: String -> String
function searchLine(s) {
var lstParts = s.split(rgxDelim);
return lstParts[0] + ' @search(' + lstParts[1]
.replace(rgxRBrkt, '\\)') + ')';
}
var rgxDelim = /\s*\t/,
rgxRBrkt = /\)/g,
outline = editor.outline,
// DIFFERENCE BETWEEN LIST OF SEARCHES
// ALREADY IN THE DOCUMENT
lstChosen = options.searches,
strInDoc = outline.evaluateItemPath('//@search')
.map(function (x) {
return x.bodyString;
})
.join('\n'),
// AND LIST OF SEARCHES CHOSEN IN DIALOG
lstSearches = strInDoc.length > 0 ? (
concatMap(function (x) {
var strSearch = searchLine(x);
return strInDoc.indexOf(strSearch) === -1 ? (
[strSearch]
) : [];
}, lstChosen)) : lstChosen.map(searchLine);
// MAYBE ADD NEW SEARCHES TO THE ACTIVE DOCUMENT
if (options.addSearches && lstSearches.length > 0) {
var root = outline.root;
outline.groupUndoAndChanges(function (x) {
root.insertChildrenBefore(
ItemSerializer.deserializeItems(
lstSearches.join('\n'),
outline,
ItemSerializer.TEXTMimeType
),
root.firstChild
);
});
}
// MAYBE APPLY A SEARCH TO THE ACTIVE DOCUMENT
if (options.applySearch && (lstChosen.length === 1)) {
editor.itemPathFilter = lstChosen[0].split(rgxDelim)[1];
}
}
// JAVASCRIPT FOR AUTOMATION CONTEXT
// Reduce a list to a subset of its elements
// that is unique in terms of the equality defined by
// a supplied function - fnEq
// nubBy :: (a -> a -> Bool) -> [a] -> [a]
function nubBy(fnEq, lst) {
var x = lst.length ? lst[0] : null;
return x ? [x].concat(
nubBy(
fnEq,
lst.slice(1)
.filter(
function (y) {
return !fnEq(x, y);
}
)
)
) : [];
}
// concatMap :: (a -> [b]) -> [a] -> [b]
function concatMap(f, xs) {
return [].concat.apply([], xs.map(f));
}
// fileExists :: String -> Bool
function fileExists(strPath) {
var fm = $.NSFileManager.defaultManager,
error = $();
fm.attributesOfItemAtPathError(
ObjC.unwrap($(strPath)
.stringByExpandingTildeInPath),
error
);
return error.code === undefined;
}
// String -> String
function textFileContents(strPath) {
return $.NSString.stringWithContentsOfFile(strPath);
}
// Array of any default filepath(s) given/listed in options at end
var varDefault = dctOptions.defaultFiles,
lstPaths = varDefault ? (
concatMap(function (x) {
var s = x.trim();
return s.length > 0 ? [s] : [];
}, (varDefault instanceof Array ? varDefault : [varDefault]))
) : [];
// COMBINE ALL SEARCHES IN THE DEFAULT FILE(S)
var tp3 = Application("TaskPaper"),
ds = tp3.documents(),
strText = lstPaths.reduce(function (a, strPath) {
return a + ((fileExists(strPath)) ? textFileContents($(
strPath)
.stringByExpandingTildeInPath)
.js : '');
}, ''),
lstSearches = strText.split(/[\n\r]+/)
.filter(function (s) {
return s.indexOf('@search') !== -1;
})
// WITH ALL SEARCHES IN ALL OPEN TP3 DOCS
.concat(concatMap(function (d) {
return d.evaluate({
script: fnFindSearches.toString(),
})
}, ds))
// EXTRACTING SEARCH NAMES AND PATHS
.map(function (x) {
var lstParts = x.split('@search('),
strPath = lstParts[1];
return {
name: lstParts[0].split(/\W/)
.filter(function (str) {
return str.length > 0;
})
.join(' '),
path: strPath.substr(0, strPath.lastIndexOf(')'))
.replace(/\\\)/g, ')')
}
}),
// REDUCING THEM DOWN TO A UNIQUE LIST OF PATHS
lstMenu = nubBy(function (a, b) {
return a.path === b.path;
}, lstSearches)
.sort(function (a, b) {
var strA = a.name.toLowerCase(),
strB = b.name.toLowerCase();
return strA < strB ? -1 : (strA > strB ? 1 : 0);
});
// OFFERING THEM AS A MENU
if (lstMenu.length > 0) {
var su = Application("SystemUIServer"),
sa = (su.includeStandardAdditions = true, su),
// DISPLAYING THE APPLY AND/OR ADD OPTIONS
strApply = dctOptions.applySearch ? 'apply' : '',
strAdd = dctOptions.addSearches ? 'add' : '',
strAnd = (strApply.length && strAdd.length) ? ' and ' : '',
strVerbs = strApply + strAnd + strAdd,
// AIMING TO FORMAT NAME AND PATH IN
// DISTINCT COLUMNS
lngChars = lstMenu.reduce(function (a, b) {
var lng = b.name.length;
return lng > a ? lng : a
}, 0),
lst = lstMenu.map(function (x) {
var strName = x.name,
strPad = Array(lngChars + 1)
.join(' ');
return (strName + strPad)
.slice(0, 12) + '\t' + x.path;
});
// AND GETTING THE USER'S CHOICE
sa.activate();
var varChoice = sa.chooseFromList(lst, {
withTitle: "TaskPaper 3 saved searches",
withPrompt: 'Gathered from all open docs' + (
lstPaths.length > 0 ? ',\nand from:\n' +
lstPaths.map(
function (strPath) {
return '\n\t' + strPath;
}) : ''
) + '\n\nChoose search(es) to add:',
defaultItems: lst[0],
okButtonName: 'OK',
cancelButtonName: 'Cancel',
multipleSelectionsAllowed: dctOptions.addSearches,
emptySelectionAllowed: true
}),
lstChoice = varChoice ? varChoice : [];
// IF A SEARCH HAS BEEN CHOSEN ...
if (lstChoice.length > 0) {
tp3.activate();
// APPLY AND/OR ADD THE SEARCH
// (IT IS NOT ALREADY IN THE DOC)
// SEE OPTIONS
return ds.length ? ds[0].evaluate({
script: fnUseSearches.toString(),
withOptions: {
searches: lstChoice,
applySearch: dctOptions.applySearch,
addSearches: dctOptions.addSearches
}
}) : undefined;
}
}
})({
// Bool
applySearch: false, // Filter active doc with chosen search (if single)
// Bool
addSearches: true, // Add chosen search(es) if not in active doc
// String | [String] | undefined
defaultFiles: Application("Keyboard Maestro Engine")
.variables['tp3MainFilePaths'].value()
.split(/[\n\r]+/)
});