Attribute browsing menu for Tinderbox 8 JS scripts

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();
})();

If anyone wants to experiment with an AppleScript along these lines, here is the JSON file and a few basic AppleScript functions to get you started. (AS source code below)

tbxAttribs.json.zip (4.2 KB)

readAttribsJSONAS-001.applescript.zip (2.3 KB)

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    tell application "Finder" to set fpHere to (container of (path to me) as alias)
    
    -- Replace the first |id| with a function for reporting a 
    -- 'Left' message about finding or parsing tbxAttribs.json 
    -- (which needs to be in the same folder as this script)
    
    -- and replace the second |id| with a function making use of the 
    -- the record containing Categorized and A-Z listings of 
    -- Tinderbox 8 System Attributes
    
    either(|id|, |id|, ¬
        bindLR(readFileLR((POSIX path of fpHere) & "tbxAttribs.json"), ¬
            JSONParseLR))
end run


-- GENERIC -----------------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- Left :: a -> Either a b
on |Left|(x)
    {type:"Either", |Left|:x, |Right|:missing value}
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    {type:"Either", |Left|:missing value, |Right|:x}
end |Right|

-- bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
on bindLR(m, mf)
    if missing value is not |Right| of m then
        mReturn(mf)'s |λ|(|Right| of m)
    else
        m
    end if
end bindLR

-- either :: (a -> c) -> (b -> c) -> Either a b -> c
on either(lf, rf, e)
    if isRight(e) then
        tell mReturn(rf) to |λ|(|Right| of e)
    else
        tell mReturn(lf) to |λ|(|Left| of e)
    end if
end either

-- id :: a -> a
on |id|(x)
    -- The identity function. The argument unchanged.
    x
end |id|

-- isRight :: Either a b -> Bool
on isRight(x)
    set dct to current application's ¬
        NSDictionary's dictionaryWithDictionary:x
    (dct's objectForKey:"type") as text = "Either" and ¬
        (dct's objectForKey:"Left") as list = {missing value}
end isRight

-- jsonParseLR :: String -> Either String a
on JSONParseLR(s)
    set ca to current application
    set {x, e} to ca's NSJSONSerialization's ¬
        JSONObjectWithData:((ca's NSString's stringWithString:s)'s ¬
            dataUsingEncoding:(ca's NSUTF8StringEncoding)) ¬
            options:0 |error|:(reference)
    if x is missing value then
        |Left|(e's localizedDescription() as string)
    else
        if 1 = (x's isKindOfClass:(ca's NSArray)) as integer then
            |Right|(x as list)
        else
            |Right|(item 1 of (x as list))
        end if
    end if
end JSONParseLR

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- readFileLR :: FilePath -> Either String String
on readFileLR(strPath)
    set ca to current application
    set e to reference
    set {s, e} to (ca's NSString's ¬
        stringWithContentsOfFile:((ca's NSString's ¬
            stringWithString:strPath)'s ¬
            stringByStandardizingPath) ¬
            encoding:(ca's NSUTF8StringEncoding) |error|:(e))
    if e is missing value then
        |Right|(s as string)
    else
        |Left|((localizedDescription of e) as string)
    end if
end readFileLR
1 Like

Thanks for this! I studied but don’t understand the AppleScript “functions.” So I came up with a more basic AppleScripty approach based on lists, which to my surprise turns out to be relatively concise. Its A-Z list includes User Attributes and also sorts somewhat differently. The script is self-contained. It doesn’t read an external file.

"Click to reveal script (revised for better navigation)
use framework "Foundation"
use scripting additions

-- get list of all attributes in the Tinderbox document
tell application "Tinderbox 8" to tell front document to set allAttribs to attributes's name
set allAttribs to sortList(allAttribs) -- and sort it

-- assemble a flat (uncategorized) list of all system attributes from the category lists below
set sysAttribs to AI & Agent & Appearance & Composites & |Events| & General & Grid & HTML & Iris & Map & Net & |Outline| & People & |References| & Sandbox & Scrivener & Sorting & Storyspace & TextFormat & Textual & Watch & Weblog

-- get attributes that are not in the list of system attributes ==> user attributes
set User to notInList(allAttribs, sysAttribs)

-- assemble a "categorized" list of lists including the derived members of the User category
set allAttribsByCat to {AI, Agent, Appearance, Composites, |Events|, General, Grid, HTML, Iris, Map, Net, |Outline|, People, |References|, Sandbox, Scrivener, Sorting, Storyspace, TextFormat, Textual, User, Watch, Weblog}

set {response1, response2, response3, cancelled2, cancelled3} to {false, false, false, false, false}
set dialog1Prompts to {"User attributes (" & User's length & ")", "System attributes by Category", "System attributes A-Z"}

repeat until response3 is not false
	if cancelled3 is true then -- show dialog 2 again if 'cancel' at third dialog
		set response2 to showListDialog(Categories, "Select:", "Categories", false)
		if response2 is not false then set catIndex to listPosition(response2's first item, Categories)
	end if
	repeat until response2 is not false
		set response1 to showListDialog(dialog1Prompts, "Choose:", "Tinderbox Attributes", false)
		if response1 is false then return -- exit script if 'cancel' at first dialog
		set responseIndex to listPosition(response1 as string, dialog1Prompts)
		if responseIndex is 1 then -- User Attributes
			set response2 to showListDialog(User, "Choose one or more:", "User", true)
		else
			if responseIndex is 2 then -- By Category
				set response2 to showListDialog(Categories, "Select:", "Categories", false)
				if response2 is not false then set catIndex to listPosition(response2's first item, Categories)
			else
				if responseIndex is 3 then -- Attributes A-Z
					set response2 to showListDialog(allAttribs, "Choose one or more of " & allAttribs's length, "Attributes A-Z", true)
				end if
			end if
		end if
	end repeat
	if responseIndex is in {1, 3} then return response2 -- if only 2 levels then return second response
	set response3 to showListDialog(allAttribsByCat's item catIndex, "Choose one or more:", Categories's item catIndex, true)
	set cancelled3 to response3 is false -- repeats loop if user cancelled
end repeat

return response3


##### HANDLERS (a.k.a subroutines, functions) ####

to showListDialog(aList, aPrompt, aTitle, multAllowed)
	if multAllowed then
		choose from list aList with prompt aPrompt with title aTitle with multiple selections allowed
	else
		choose from list aList with prompt aPrompt with title aTitle
	end if
end showListDialog

to notInList(listA, listB)
	set newList to {}
	repeat with a in listA
		if a is not in listB then set end of newList to a's contents
	end repeat
	return newList
end notInList

to listPosition(i, aList)
	repeat with n from 1 to count aList
		if aList's item n is i then return n
	end repeat
	return 0
end listPosition

to sortList(aList)
	((current application's NSArray's arrayWithArray:aList)'s ¬
		sortedArrayUsingSelector:"compare:") as list
end sortList


### DATA -  lists of Tinderbox attribute names by category

property Categories : {"AI", "Agent", "Appearance", "Composites", "Events", "General", "Grid", "HTML", "Iris", "Map", "Net", "Outline", "People", "References", "Sandbox", "Scrivener", "Sorting", "Storyspace", "TextFormat", "Textual", "User", "Watch", "Weblog"}

property AI : {"NLNames", "NLOrganizations", "NLPlaces"}

property Agent : {"AgentAction", "AgentCaseSensitive", "AgentPriority", "AgentQuery", "CleanupAction"}

property Appearance : {"AccentColor", "Badge", "BadgeMonochrome", "BadgeSize", "Border", "BorderBevel", "BorderColor", "BorderDash", "CaptionAlignment", "CaptionColor", "CaptionFont", "CaptionOpacity", "CaptionSize", "Color", "Color2", "Flags", "Opacity", "Pattern", "PlotColor", "Shadow", "ShadowBlur", "ShadowColor", "ShadowDistance", "Shape"}

property Composites : {"Associates", "IsComposite", "IsMultiple", "NeverComposite", "OnJoin", "Role"}

property |Events| : {"DueDate", "EndDate", "StartDate", "TimelineAliases", "TimelineBand", "TimelineBandLabelColor", "TimelineBandLabelOpacity", "TimelineBandLabels", "TimelineColor", "TimelineDescendants", "TimelineEnd", "TimelineEndAttribute", "TimelineGridColor", "TimelineMarker", "TimelineScaleColor", "TimelineScaleColor2", "TimelineStart", "TimelineStartAttribute"}

property General : {"AdornmentCount", "Aliases", "Caption", "Checked", "ChildCount", "ClusterTerms", "Container", "Created", "Creator", "DescendantCount", "DisplayExpression", "DisplayExpressionDisabled", "DisplayName", "Edict", "EdictDisabled", "File", "HoverExpression", "HoverImage", "HoverOpacity", "ID", "InboundLinkCount", "IsAdornment", "IsAlias", "IsPrototype", "Modified", "Name", "OnAdd", "OnRemove", "OutboundLinkCount", "OutlineDepth", "OutlineOrder", "Path", "PlainLinkCount", "Private", "Prototype", "PrototypeBequeathsChildren", "ReadCount", "Rule", "RuleDisabled", "Searchable", "SelectionCount", "SiblingOrder", "Subtitle", "TextLinkCount", "User", "WebLinkCount"}

property Grid : {"GridColor", "GridColumns", "GridLabelFont", "GridLabelSize", "GridLabels", "GridOpacity", "GridRows"}

property HTML : {"HTMLBoldEnd", "HTMLBoldStart", "HTMLCloud1End", "HTMLCloud1Start", "HTMLCloud2End", "HTMLCloud2Start", "HTMLCloud3End", "HTMLCloud3Start", "HTMLCloud4End", "HTMLCloud4Start", "HTMLCloud5End", "HTMLCloud5Start", "HTMLDontExport", "HTMLEntities", "HTMLExportAfter", "HTMLExportBefore", "HTMLExportChildren", "HTMLExportCommand", "HTMLExportExtension", "HTMLExportFileName", "HTMLExportFileNameSpacer", "HTMLExportPath", "HTMLExportTemplate", "HTMLFileNameLowerCase", "HTMLFileNameMaxLength", "HTMLFirstParagraphEnd", "HTMLFirstParagraphStart", "HTMLFont", "HTMLFontSize", "HTMLImageEnd", "HTMLImageStart", "HTMLIndentedParagraphEnd", "HTMLIndentedParagraphStart", "HTMLItalicEnd", "HTMLItalicStart", "HTMLLinkExtension", "HTMLListEnd", "HTMLListItemEnd", "HTMLListItemStart", "HTMLListStart", "HTMLMarkDown", "HTMLMarkupText", "HTMLOrderedListEnd", "HTMLOrderedListItemStart", "HTMLOrderedListStart", "HTMLOrderedsListItemEnd", "HTMLOverwriteImages", "HTMLParagraphEnd", "HTMLParagraphStart", "HTMLPreviewCommand", "HTMLQuoteHTML", "HTMLStrikeEnd", "HTMLStrikeStart", "HTMLUnderlineEnd", "HTMLUnderlineStart", "IsTemplate"}

property Iris : {"IrisAngle", "IrisRadius"}

property Map : {"AdornmentFont", "Base", "Bend", "Direction", "Fill", "FillOpacity", "Height", "HoverFont", "InteriorScale", "LeafBase", "LeafBend", "LeafDirection", "LeafTip", "Lock", "MapBackgroundAccentColor", "MapBackgroundColor", "MapBackgroundColor2", "MapBackgroundFill", "MapBackgroundFillOpacity", "MapBackgroundPattern", "MapBackgroundShadow", "MapBodyTextColor", "MapBodyTextSize", "MapScrollX", "MapScrollY", "MapTextSize", "NameAlignment", "NameBold", "NameColor", "NameFont", "NameLeading", "NameStrike", "PlotBackgroundColor", "PlotBackgroundOpacity", "PlotColorList", "Sticky", "SubtitleColor", "SubtitleOpacity", "SubtitleSize", "TableExpression", "TableHeading", "Tip", "TitleHeight", "TitleOpacity", "Width", "Xpos", "Ypos"}

property Net : {"AutoFetch", "AutoFetchCommand", "LastFetched", "NoteURL", "RSSChannelTemplate", "RSSItemLimit", "RSSItemTemplate", "RawData", "URL", "ViewInBrowser"}

property |Outline| : {"OutlineBackgroundColor", "OutlineColorSwatch", "OutlineTextSize", "PrototypeHighlightColor", "Separator"}

property People : {"AIM", "Address", "City", "Country", "District", "Email", "FormattedAddress", "FullName", "GeocodedAddress", "Latitude", "Longitude", "Organization", "PostalCode", "State", "Telephone", "Twitter"}

property |References| : {"Abstract", "AccessDate", "ArticleTitle", "Author2", "Author3", "Author4", "Authors", "BookTitle", "CallNumber", "DOI", "Edition", "ISBN", "Issue", "Journal", "Pages", "PublicationCity", "PublicationYear", "Publisher", "RefFormat", "RefKeywords", "RefType", "ReferenceRIS", "ReferenceTitle", "ReferenceURL", "SourceCreated", "SourceModified", "Tags", "Volume"}

property Sandbox : {"MyBoolean", "MyColor", "MyDate", "MyInterval", "MyList", "MyNumber", "MySet", "MyString"}

property Scrivener : {"ScrivenerID", "ScrivenerKeywords", "ScrivenerLabel", "ScrivenerLabelID", "ScrivenerNote", "ScrivenerStatus", "ScrivenerStatusID", "ScrivenerType"}

property Sorting : {"Sort", "SortAlso", "SortAlsoTransform", "SortBackward", "SortBackwardAlso", "SortTransform"}

property Storyspace : {"BeforeVisit", "ChosenWord", "Deck", "OnVisit", "Requirements", "ResetAction", "Visits"}

property TextFormat : {"HideKeyAttributes", "KeyAttributeDateFormat", "KeyAttributeFont", "KeyAttributeFontSize", "KeyAttributes", "LeftMargin", "LineSpacing", "NoSpelling", "ParagraphSpacing", "RightMargin", "ShowTitle", "SmartQuotes", "Tabs", "TextAlign", "TextBackgroundColor", "TextColor", "TextFont", "TextFontSize", "TextPaneRatio", "TextPaneWidth", "TextSidebar", "TextWindowHeight", "TextWindowWidth", "TitleBackgroundColor", "TitleFont", "TitleForegroundColor"}

property Textual : {"ImageCount", "ReadOnly", "Text", "TextExportTemplate", "TextLength", "WordCount"}

property Watch : {"DEVONthinkGroup", "DEVONthinkLabel", "EvernoteNotebook", "NotesFolder", "NotesID", "NotesModified", "SimplenoteKey", "SimplenoteModified", "SimplenoteSync", "SimplenoteTags", "SimplenoteVersion", "SourceURL", "UUID", "WatchFolder"}

property Weblog : {"WeblogPostID", "mt_allow_comments", "mt_allow_pings", "mt_convert_breaks", "mt_keywords"}

Doing this provided a newfound appreciation for the number of attributes in Tinderbox!

1 Like

Good work :slight_smile:

(One extra thing that might reduce friction in day to day use would be allowing the user to back out of one submenu, and then drill down into another, without having to restart the script at the top level.

i.e. allow Cancel to retreat from a child menu back up to a parent menu, rather than always interpreting it as exiting the script entirely).

FWIW an AppleScript variant roughly assembled from pre-fab flat-pack components, including a generic treeMenu function (for moving up and down in a nested menu system), might look something like this:

tbxAttributesTreeMenuAS.applescript.zip (9.5 KB)

(The treeMenu function itself, extracted from the code, looks like this, and can also be found at GitHub - RobTrew/prelude-applescript: Generic functions for macOS scripting with Applescript – function names as in Hoogle, or in a JS version at: GitHub - RobTrew/prelude-jxa: Generic functions for macOS and iOS scripting in Javascript – function names as in Hoogle):

Click for source of single function
-- treeMenu :: Tree String -> IO [String]
on treeMenu(tree)
    script go
        on |λ|(tree)
            set menuTitle to root(tree)
            set subTrees to nest(tree)
            set menuItems to map(my root, subTrees)
            set blnNoSubMenus to {} = concatMap(my nest, subTrees)
            
            script menuCancelledOrChoiceMade
                on |λ|(statusAndChoices)
                    (not my fst(statusAndChoices)) or ({} ≠ my snd(statusAndChoices))
                end |λ|
            end script
            
            script choicesOrSubMenu
                on |λ|(choices)
                    set k to item 1 of choices
                    script match
                        on |λ|(x)
                            k = my root(x)
                        end |λ|
                    end script
                    set chosenSubTree to (Just of find(match, subTrees))
                    if {} ≠ nest(chosenSubTree) then
                        |Right|(|λ|(chosenSubTree) of go)
                    else
                        |Right|(choices)
                    end if
                end |λ|
            end script
            
            script nothingFromThisMenu
                on |λ|(_)
                    Tuple(false, {})
                end |λ|
            end script
            
            script selectionsFromThisMenu
                on |λ|(xs)
                    Tuple(true, xs)
                end |λ|
            end script
            
            script nextStepInNestedMenu
                on |λ|(statusAndChoice)
                    either(nothingFromThisMenu, selectionsFromThisMenu, ¬
                        bindLR(showMenuLR(blnNoSubMenus, menuTitle, menuItems), ¬
                            choicesOrSubMenu))
                end |λ|
            end script
            
            snd(|until|(menuCancelledOrChoiceMade, ¬
                nextStepInNestedMenu, ¬
                Tuple(true, {}))) -- (Status, Choices) pair
        end |λ|
    end script
    |λ|(tree) of go
end treeMenu

AppleScript and JavaScript versions in parallel

1 Like

Thanks for posting these. It’s obvious I didn’t “peek at the answer” when I revised my script above to back out and drill down again. I used a good old repeat loop with no recursion. It’s a pedestrian approach, somewhat messy and not easily generalized. Expert coders will wince. But it works for this particular case!

Its main virtues are that it’s relatively concise and doesn’t require an understanding of script objects within a script. I’m still working to get my head around those. Your examples will help.

I find it fascinating that the Tinderbox scripting part can be done in just one line:

tell application "Tinderbox 8" to tell front document to set allAttribs to attributes's name

All the rest is just basic AppleScript list manipulation and managing dialog boxes.

1 Like

Looks good !

On the notion of brevity and conciseness as virtues in scripts, I think we probably all optimise for different things :slight_smile:

( There is an interesting argument here for loquacity in code, or at least for using wordy variable names, in honour of self-explanatory terms like Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz

German Naming Convention)

I personally like to optimise for fast assembly and refactoring, and also for high levels of code reuse.

Without that, I find that (for me at least) the various XKCD jokes about scripting and automation soon come into play : -)

https://xkcd.com/1319/
https://xkcd.com/1205/
https://xkcd.com/1445/
etc etc

The Blaise Pascal quote comes to mind:

Je n’ai fait celle-ci plus longue que parce que je n’ai pas eu le loisir de la faire plus courte.

([This letter/script is] long for want of the leisure to shorten it)

(But sometimes scripting is just its own reward, and a good way to relax, of course)

1 Like

Not sure if this the right forum, and happy to continue this discussion off-line if it’s of any interest, but briefly:

Script objects just add magic powers to ordinary handlers (turn them into ‘first class’ functions)

Let me explain:

When I want to assemble something quickly, and reach for the ‘Lego’ tray, the most helpful reusable pieces turn out to be:

  • map
  • filter
  • fold (called reduce in some languages like JS, and always available in left-to-right and right-to-left variants)

map

map example
-- double :: Num -> Num
on double(x)
    2 * x
end double

on run
    set xs to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    map(double, xs)
    
    --> {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
end run


-- GENERIC ---------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    -- The list obtained by applying f
    -- to each element of xs.
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

filter

filter example
-- even :: Int -> Bool
on even(x)
    0 = x mod 2
end even


on run
    set xs to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    filter(even, xs)
    
    --> {2, 4, 6, 8, 10}
end run


-- GENERIC ---------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- filter :: (a -> Bool) -> [a] -> [a]
on filter(f, xs)
    tell mReturn(f)
        set lst to {}
        set lng to length of xs
        repeat with i from 1 to lng
            set v to item i of xs
            if |λ|(v, i, xs) then set end of lst to v
        end repeat
        return lst
    end tell
end filter

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

fold

foldl example (foldr would be right to left)

-- add :: Num -> Num -> Num
on add(a, b)
    a + b
end add


-- multiply :: Num -> Num -> Num
on multiply(a, b)
    a * b
end multiply


on run
    set xs to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    {foldl(add, 0, xs), ¬
        foldl(multiply, 1, xs)}
    
    --> {55, 3628800}
    -- 55 = {0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10} 
    -- 3628800 = {1 * 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10} 
end run


-- GENERIC ---------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from 1 to lng
            set v to |λ|(v, item i of xs, i, xs)
        end repeat
        return v
    end tell
end foldl

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

The points to note :

  1. What map, filter and fold/reduce have in common is that each takes a first argument which is itself a handler
  2. Passing handlers as arguments doesn’t work in AppleScript (try it :slight_smile: ) A function which can be passed as an argument, just like a string or a number or any other value is called a “first class function”. AppleScript handlers are not, on their own, first class functions.
  3. An AppleScript handler (of any name) wrapped in a Script object, can be used as a first class function.

We could rewrite the all the examples above using functions written as script wrappers around single handlers:

Writing functions as script-wrapped handlers
script double
    on |λ|(x)
        2 * x
    end |λ|
end script

script even
    on |λ|(x)
        0 = x mod 2
    end |λ|
end script

script add
    on |λ|(a, b)
        a + b
    end |λ|
end script

script multiply
    on |λ|(a, b)
        a * b
    end |λ|
end script

on run
    set xs to {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    {map(double, xs), ¬
        filter(even, xs), ¬
        foldl(add, 0, xs), ¬
        foldl(multiply, 1, xs)}
    
    --> {{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}, {2, 4, 6, 8, 10}, 55, 3628800}
end run

-- GENERIC ---------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    -- The list obtained by applying f
    -- to each element of xs.
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- filter :: (a -> Bool) -> [a] -> [a]
on filter(f, xs)
    tell mReturn(f)
        set lst to {}
        set lng to length of xs
        repeat with i from 1 to lng
            set v to item i of xs
            if |λ|(v, i, xs) then set end of lst to v
        end repeat
        return lst
    end tell
end filter

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from 1 to lng
            set v to |λ|(v, item i of xs, i, xs)
        end repeat
        return v
    end tell
end foldl


-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

And what does that mysterious mReturn function do ?

It just ensures that a handler is script-wrapped (and therefore ‘first-class’, i.e. usable as the argument to another function).


In short, map filter and fold take simple and humble functions which act on single values or pairs of values, and give them super-powers (turning them into functions which act on whole lists).

In the case of languages like JS, Python etc, map filter and fold are supplied pre-baked. In AppleScript we can quickly write them ourselves, as long as we learn the trick of elevating a simple handler to a first-class function by wrapping it in a Script object.

2 Likes

So all these years when when I tell people, “Sorry, but I haven’t had time to make it shorter,” I have been paraphrasing Pascal!

I feel a little like would-be aristocrat Monsieur Jourdain in Molière’s Le Bourgeois Gentilhomme–after his tutor explains that all language is either prose or poetry–delighting in the idea that he has been speaking prose all his life. " Par ma foi ! Il y a plus de quarante ans que je dis de la prose sans que j’en susse rien."

In any case, my scripts are prosaic. Yours rhyme. So I’ll study your useful examples and explanations and try not to let this become a case of, as the Chinese say, “playing the lute to a cow” (the idea being that the cow can’t appreciate the music, so no point in going the effort).

1 Like

对牛弹琴 ?千万不是这个意思!

Monsieur Jourdain

:slight_smile:

(Apparently Mark Twain has a similar formulation to the Pascal one)

那么继续弹吧!我这个大笨牛会尽量听。:grinning:

(Pascal, Franklin, Twain, Luther, some say even Cicero. I like Pascal because that gave me a rare excuse to trot out Molière. But the Chinese may be right in claiming that 天下文章一大抄。)

I hope our linguistic digressions aren’t scaring people away from the idea that––xkcd.com jokes aside––scripts with Tinderbox can make some things easier and other things possible that weren’t possible before!

Keep the tips coming.

1 Like

怎么这么谦虚呢 ?

Absolutely agree – the new osascript interface is a wonderful development – lots of things feel suddenly easier and more in reach : -)

FWIW, another example of using foldl

(Building up a reference to a clickable sub-menu from any chain of sub-menu titles)

on run
    tell application "Tinderbox 8" to activate
    tell application "System Events"
        set appProc to first application process where name is "Tinderbox 8"
        
        click my procMenuItem(appProc, {"Stamps", "Reset Display Expression"})
    end tell
end run

-- procMenuItem :: Application Process -> [Menu Title String] -> Menu Item
on procMenuItem(oProc, ks)
    -- e.g. ks {"Format", "Font", "Kern", "Tighten"}
    tell application "System Events"
        tell oProc
            script go
                on |λ|(a, k)
                    menu item k of menu 1 of a
                end |λ|
            end script
            my foldl(go, menu bar item (item 1 of ks) of menu bar 1, rest of ks)
        end tell
    end tell
end procMenuItem


-- GENERIC -----------------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from 1 to lng
            set v to |λ|(v, item i of xs, i, xs)
        end repeat
        return v
    end tell
end foldl

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

Which in a JS version we might write, for example, as:

(() => {
    'use strict';

    // procMenuItem :: Application Process ->
    //                 [Menu Title String] -> Menu Item
    const procMenuItem = oProc => ks =>
        //  e.g. ks = ['Format', 'Font', 'Kern', 'Tighten']
        ks.slice(1).reduce(
            (a, k) => a.menus.at(0).menuItems.byName(k),
            oProc.menuBars.at(0).menuBarItems.byName(ks[0])
        );

    const
        appProc = Application('System Events')
        .applicationProcesses.where({
            name: 'Tinderbox 8'
        }).at(0);

    return (
        Application('Tinderbox 8').activate(),
        procMenuItem(appProc)(
            ['Stamps', 'Reset Display Expression']
        ).click()
    );
})();

1 Like

Excuse my ignorance, but what is the use of such scripts? What can one do with them?
I run them, I see the attributes, but it doesn’t interact in any way with my tinderbox file.

but what is the use of such scripts?

Components of useful scripts - not standalone scripts.

The API is new and we are experimenting with how to do some basic things with it. The treeMenu function in that one, for example can be copied and pasted for reuse with any nested structure arising in a use of Tinderbox, or an interaction of Tinderbox with another application.

1 Like

In an odd way I find this thread fascinating because I don’t understand a word of it. I am undoubtedly the cow to whom you are playing the lute. But I’m glad somebody can play it.

PS: I suspect there are a lot more cows than lutenists in the world.

Division of labour – cities are built on it : -)

Hope to start sharing a few simply usable instruments soon, starting with quick creation of links between two or more items selected in the GUI.

e.g.

  • one to many links (first selected item to remaining selected items, in order of selection)
  • many to one (rest to first)
  • chain of links (first selected item thru to last)
  • cycle of links (chain returning to where it started)

or perhaps reorganized, for Keyboard Maestro | LaunchBar | Alfred as:

  • one to many
  • chain
  • cycle

each with option of reversed direction.