Here is a first draft of a menu for browsing and choosing Tinderbox 8 attributes (by category, or A-Z) in JavaScript for Automation scripts. (In Script Editor, set the language tab at top right to JavaScript)
(A parallel AppleScript version could also be written).
NB This draft assumes that the tbxAttribs.json
file is in the same folder as the script file, but the menuSystem JSON could also be directly included in the script itself.
UPDATE (June 8 2019)
- A User attributes group now added to the menu
- JSON file updated
- JS source below updated.
scriptAndJSON.zip (7.9 KB)
–> ["MyBoolean","MyDate","MyInterval","MyList","MyNumber"]
JS source code
(() => {
'use strict';
// Rob Trew 2019
// Ver 0.06
// General purpose menu, for use in Tinderbox 8 scripts,
// of user attribute names and system attribute names
// (by category, and A-Z)
// NB This version assumes that the file tbxAttribs.json
// is the same folder as the script.
// main :: IO ()
const main = () => {
const nameOfJSONfileInThisScriptsFolder = 'tbxAttribs.json';
const
menuTitle = 'Tinderbox Attributes',
chosenAttributeNames = either(
// A warning that the JSON file could not be found
// and parsed from the given filePath and name,
alert(menuTitle),
// or a list of any menu choices made.
choices => choices,
bindLR(
bindLR(
readFileLR(
pathToMe(standardAdditions()) +
'/' + nameOfJSONfileInThisScriptsFolder
),
jsonParseLR
),
// The name `dct` is bound here to any dictionary
// that has been successfully parsed from a
// successful file read above.
dct => either(
// If *no* user attributes are found:
_ => treeMenu(treeFromDict(menuTitle)(dct)),
// If user attributes *are* found:
composeList([
Right,
treeMenu,
treeFromDict(menuTitle),
dictWithUserAttributesAdded(dct)
]),
frontDocUserAttributeNamesLR(
keys(dct['System attributes A-Z'])
)
)
)
);
// Make use of list of attribute names chosen
// ...
// Display list of attribute names chosen
return JSON.stringify(chosenAttributeNames);
};
// TINDERBOX ------------------------------------------
// frontDocUserAttributeNamesLR :: [String] -> Either String [String]
const frontDocUserAttributeNamesLR = sysAttrNames =>
bindLR(
tbxFrontDocLR(),
doc => Right(difference(
doc.attributes.name(),
sysAttrNames
))
);
// tbxFrontDocLR :: IO () -> Either String TBX Doc
const tbxFrontDocLR = () => {
const ds = Application('Tinderbox 8').documents;
return 0 < ds.length ? (
Right(ds.at(0))
) : Left('No documents open in Tinderbox 8');
};
// dictWithUserAttributesAdded :: Dict -> [String] -> Dict
const dictWithUserAttributesAdded = dct => userAttribNames =>
0 < userAttribNames.length ? (
Object.assign({
[
'User attributes (' +
userAttribNames.length.toString() + ')'
]: userAttribNames.reduce(
(a, x) => Object.assign(a, {
[x]: 'User attributes'
}), {}
)
}, dct)
) : dct
// MENU TREE (JS for Automation) ----------------------
// showMenuLR :: Bool -> String -> [String] -> Either String [String]
const showMenuLR = (blnMult, k, xs) =>
0 < xs.length ? (() => {
const sa = standardSEAdditions();
sa.activate();
const v = sa.chooseFromList(xs, {
withTitle: k,
withPrompt: 'Select' + (
blnMult ? ' one or more of ' +
xs.length.toString() : ':'
),
defaultItems: xs[0],
okButtonName: 'OK',
cancelButtonName: 'Cancel',
multipleSelectionsAllowed: blnMult,
emptySelectionAllowed: false
});
return Array.isArray(v) ? (
Right(v)
) : Left('User cancelled ' + k + ' menu.');
})() : Left(k + ': No items to choose from.');
// treeFromDict :: String -> Dict -> [Tree String]
const treeFromDict = rootLabel => dict => {
const go = x =>
'object' !== typeof x ? [] : (
Array.isArray(x) ? (
map(v => Node(v, []), x)
) : map(k => Node(k, go(x[k])), keys(x))
);
return Node(rootLabel, go(dict));
};
// treeMenu :: Tree String -> IO [String]
const treeMenu = tree => {
const go = t => {
const
strTitle = t.root,
subs = t.nest,
menu = map(root, subs),
blnMore = 0 < concatMap(nest, subs).length;
return until(
tpl => !fst(tpl) || !isNull(snd(tpl)),
tpl => either(
x => Tuple(false, []),
x => Tuple(true, x),
bindLR(
showMenuLR(!blnMore, strTitle, menu),
ks => {
const
k = ks[0],
chosen = find(x => k === x.root, subs)
.Just;
return Right(
isNull(chosen.nest) ? ks : go(chosen)
);
}
)
),
Tuple(true, [])
)[1]
};
return go(tree);
};
// JXA ------------------------------------------------
// alert :: String => String -> IO String
const alert = title => s => {
const
sa = Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ['OK'],
defaultButton: 'OK'
}),
s
);
};
// pathToMe :: Application -> filePath
const pathToMe = app =>
ObjC.unwrap($(app.pathTo(this)
.toString())
.stringByDeletingLastPathComponent);
// standardAdditions :: () -> Application
const standardAdditions = () =>
Object.assign(Application.currentApplication(), {
includeStandardAdditions: true
});
// standardSEAdditions :: () -> Application
const standardSEAdditions = () =>
Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
// GENERIC FUNCTIONS ----------------------------------
// https://github.com/RobTrew/prelude-jxa
// Just :: a -> Maybe a
const Just = x => ({
type: 'Maybe',
Nothing: false,
Just: x
});
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = (v, xs) => ({
type: 'Node',
root: v, // any type of value (consistent across tree)
nest: xs || []
});
// Nothing :: Maybe a
const Nothing = () => ({
type: 'Maybe',
Nothing: true,
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// and :: [Bool] -> Bool
const and = xs =>
// True unless any contained value is false.
xs.every(Boolean);
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
undefined !== m.Left ? (
m
) : mf(m.Right);
// bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
const bindMay = (mb, mf) =>
mb.Nothing ? mb : mf(mb.Just);
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (f, g) => x => f(g(x));
// composeList :: [(a -> a)] -> (a -> a)
const composeList = fs =>
x => fs.reduceRight((a, f) => f(a), x, fs);
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.reduce((a, x) => a.concat(f(x)), []);
// cons :: a -> [a] -> [a]
const cons = (x, xs) => [x].concat(xs);
// difference :: Eq a => [a] -> [a] -> [a]
const difference = (xs, ys) => {
const s = new Set(ys);
return xs.filter(x => !s.has(x));
};
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = (fl, fr, e) =>
'Either' === e.type ? (
undefined !== e.Left ? (
fl(e.Left)
) : fr(e.Right)
) : undefined;
// find :: (a -> Bool) -> [a] -> Maybe a
const find = (p, xs) => {
for (let i = 0, lng = xs.length; i < lng; i++) {
const v = xs[i];
if (p(v)) return Just(v);
}
return Nothing();
};
// fst :: (a, b) -> a
const fst = tpl => tpl[0];
// id :: a -> a
const id = x => x;
// isNull :: [a] -> Bool
// isNull :: String -> Bool
const isNull = xs =>
Array.isArray(xs) || ('string' === typeof xs) ? (
1 > xs.length
) : undefined;
// jsonParseLR :: String -> Either String a
const jsonParseLR = s => {
try {
return Right(JSON.parse(s));
} catch (e) {
return Left(`${e.message} (line:${e.line} col:${e.column})`);
}
};
// keys :: Dict -> [String]
const keys = Object.keys;
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) =>
(Array.isArray(xs) ? (
xs
) : xs.split('')).map(f);
// nest :: Tree a -> [a]
const nest = tree => tree.nest;
// readFileLR :: FilePath -> Either String String
const readFileLR = fp => {
const
e = $(),
uw = ObjC.unwrap,
s = uw(
$.NSString.stringWithContentsOfFileEncodingError(
$(fp)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
)
);
return undefined !== s ? (
Right(s)
) : Left(uw(e.localizedDescription));
};
// reverse :: [a] -> [a]
const reverse = xs =>
'string' !== typeof xs ? (
xs.slice(0).reverse()
) : xs.split('').reverse().join('');
// root :: Tree a -> a
const root = tree => tree.root;
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// snd :: (a, b) -> b
const snd = tpl => tpl[1];
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = (p, f, x) => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// MAIN ---
return main();
})();