Script: multi-document project menu (and using a JSA Library)


#1

A draft script and library function experimenting with:

  1. A single menu of all open documents and projects
  2. Using JavaScript for Automation’s Library object.

selectProject-002.zip (5.1 KB)

The zip contains two scripts, one of which (selectProject-002.scpt) uses a menu provided by a library function in the other (projectMenu.scpt)

Before you can run selectProject-002.scpt, you must first copy projectMenu.scpt to the following folder:

~/Library/Script Libraries/

(See the section on using libraries in the JavaScript for Automation release notes).

JavaScript source of the two scripts:

selectProject-002.scpt

// CHOOSE FROM MENU OF ALL OPEN DOCUMENTS AND THEIR PROJECTS
// 1. ACTIVATES CHOSEN DOCUMENT
// 2. SELECTS CHOSEN PROJECT 
//    (ENSURING THAT THE PROJECT AND AT LEAST ITS IMMEDIATE CHILDREN ARE VISIBLE)

// Ver 0.02 Rob Trew @complexpoint 2016-04-18

//INSTALLATION:

// This script depends on another script for the all-document menu:

// REQUIRES A 'SCRIPT LIBRARY PATH; TO CONTAIN the projectMenu.scpt library script
// WHERE 'script library path' =
//      ~/Library/Script Libraries/
//      OR (El Capitan) to a location specified with the OSA_LIBRARY_PATH environment variable
// e.g. OSA_LIBRARY_PATH='/opt/local/Script Libraries:/usr/local/Script Libraries'

(function () {
    'use strict';

    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {

        // find :: (a -> Bool) -> [a] -> Maybe a
        function find(f, xs) {
            for (var i = 0, lng = xs.length; i < lng; i++) {
                if (f(xs[i])) return xs[i];
            }
            return undefined;
        }

        var outlineID = options.outlineID,
            outline = find(
                function (o) {
                    return o.id === outlineID;
                },
                Outline.getOutlines()
            );

        if (outline) {
            var ed = OutlineEditor.getOutlineEditorForOutline(outline),
                project = outline.getItemForID(options.projectID),
                strProject = project.bodyString;

            !ed.isDisplayed(project) && ed.forceDisplayed(project);
            project.hasChildren && ed.setExpanded(project);

            ed.moveSelectionToItems(
                project, 0,
                project, strProject.length
            );

            return strProject;
        }
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // raiseWin :: Window -> Maybe Application -> ()
    function raiseWin(oWin, appSE) {
        (appSE || Application("System Events"))
        .perform(oWin.actions.byName('AXRaise'));

        oWin.attributes.byName('AXParent')
            .value()
            .frontmost = true;
    }

    // find :: (a -> Bool) -> [a] -> Maybe a
    function find(f, xs) {
        for (var i = 0, lng = xs.length; i < lng; i++) {
            if (f(xs[i])) return xs[i];
        }
        return undefined;
    }

    // MAIN


    // REQUIRES A 'SCRIPT LIBRARY PATH; TO CONTAIN the projectMenu.scpt library script
    // WHERE 'script library path' =
    //      ~/Library/Script Libraries/
    //      OR (El Capitan) to a location specified with the OSA_LIBRARY_PATH environment variable
    // e.g. OSA_LIBRARY_PATH='/opt/local/Script Libraries:/usr/local/Script Libraries'

    var lib = Library('projectMenu'),
        dctIds = lib.chooseDocOrProject(
            'Jump to open documents and projects');

    if (dctIds) {

        // Either chosen doc or active doc
        var tp = Application("TaskPaper"),
            se = Application("System Events"),
            strPath = dctIds.path,
            strProjectID = dctIds.projectID,

            // Purge any null to allow for withOptions use
            strOutlineID = dctIds.outlineID || undefined,

            dctDoc = find(

                // using predicate function:
                function (dct) {
                    var doc = dct.doc,
                        maybeFile = doc.file();

                    return maybeFile ? maybeFile
                        .toString() === strPath :
                        (strOutlineID === doc.evaluate({
                            script: 'function (e) {return e.outline.id}'
                        }));
                },

                // in list:
                tp.documents()
                .map(function (d, i) {
                    return {
                        index: i,
                        doc: d
                    };
                })
            );

        tp.activate();
        raiseWin(
            se.applicationProcesses.where({
                name: 'TaskPaper'
            })[0].windows.at(dctDoc.index), se
        );

        // SELECT SPECIFIC PROJECT IF CHOSEN IN MENU
        return strProjectID ? dctDoc.doc.evaluate({
            script: TaskPaperContext.toString(),
            withOptions: {
                outlineID: strOutlineID || undefined,
                projectID: strProjectID
            }
        }) : undefined;
    }

})();

projectMenu.scpt

// Library file

// Provides a menu of all open documents and projects
// through the chooseDocOrProject(strMenuTitle) function

// INSTALLATION:
//      Save as a .scpt file (File > Save > File Format > Script) to
//      ~/Library/Script Libraries/

// Ver 0.02 Rob Trew @complexpoint 2016-04-18

// chooseDocOrProject :: String -> IO Menu -> 
//        Maybe {outlineID: String, projectID:String, path:String}
function chooseDocOrProject(strMenuTitle) {
    'use strict';
    
    var strTitle = strMenuTitle || 'Open documents and projects';

    // TASKPAPER CONTEXT

    // projList :: editor -> [{oIndex:Int, pIndex:Int, depth:Int, 
    //       name:String, oID:String, pID: String}]
    function projList(editor, options) {

        // projectIndex :: [{id: name: projects: [id: name: depth:]}]
        // -> [{outlineIndex:Int, projectIndex:Int depth:Int 
        //          name:String outlineID:String projID:String }]
        function projectIndex(lstOutlineProjects) {
            var intIndex = 0;

            return lstOutlineProjects
                .reduce(function (a, dctOutline, iOutline, l) {
                    var indexOutline = iOutline + 1;

                    a.push({
                        oIndex: indexOutline,
                        pIndex: undefined,
                        depth: 0,

                        name: dctOutline.name,
                        path: dctOutline.path,
                        oID: dctOutline.id,
                        pID: undefined
                    });

                    return a.concat(dctOutline.projects
                        .map(function (dctProj, iProject) {

                            return {
                                oIndex: indexOutline,
                                pIndex: iProject + 1,
                                depth: dctProj.depth,

                                name: dctProj.name,
                                path:dctProj.path,
                                
                                oID: dctOutline.id,
                                pID: dctProj.id
                            };
                        }));
                }, []);
        }

        // projectsInAllOpenDocs() :: () -> 
        //      [{id: String, path: String, Name: String, projects: 
        //              [{ idProj: String, projName:String, depth:Int }]}]
        function projectsInAllOpenDocs(blnOutlineSort, blnProjSort) {

            function nameSort(a, b) {
                var strA = a.name.toLowerCase(),
                    strB = b.name.toLowerCase();

                return strA < strB ? -1 : (
                    strA > strB ? 1 : 0
                );
            }

            function nonSort() {
                return 0;
            }

            var rgxPathDelim = /\//;

            // ALL OUTLINES OPEN IN TASKPAPER 3
            return Outline.getOutlines()
                .map(function (o) {
                    var strPath = o.getPath();

                    // KEY DETAILS AND PROJECTS OF THESE OUTLINES
                    return {
                        id: o.id,
                        path: strPath,
                        name: strPath ? strPath.split(rgxPathDelim)
                            .pop() : '<document not saved>',

                        // PROJECTS OF THIS OUTLINE (ID, TEXT)
                        projects: o.evaluateItemPath(
                                '//@type=project'
                            )
                            .map(function (p) {
                                return {
                                    id: p.id,
                                    name: p.bodyContentString,
                                    path: strPath,
                                    depth: p.depth
                                };
                            })

                        // THESE PROJECTS POSSIBLY SORTED BY NAME
                            .sort(blnProjSort ? nameSort : nonSort)
                    };
                })

            // THESE OUTLINES POSSIBLY SORTED BY NAME
            .sort(blnOutlineSort ? nameSort : nonSort);
        }

        return projectIndex(
            projectsInAllOpenDocs(
                options.sortOutlines,
                options.sortProjects
            )
        );
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // projectChoice :: [{oIndex:Int, pIndex:Int, oID:String, 
    //          pID:String, depth:Int, name:String}] -> 
    //          {idOutline:String, idProject:String}
    function projectChoice(lstProj) {

        // menuChoiceIDs :: String -> 
        //      [{oIndex:Int, pIndex:Int, oID:String, pID:String}] -> 
        //          {idOutline:String, idProject:String}
        function menuChoiceIDs(strMenuItem, lstProjects) {
            var lstIndex = strMenuItem.split(' ')[0].trim()
                .split(' ')[0].split('.'),

                iOutline = parseInt(lstIndex[0], 10),
                strProjIndex = lstIndex[1],
                iProj = strProjIndex.length > 0 ? parseInt(
                    strProjIndex, 10
                ) : undefined,

                dctProj = find(function (x) {
                    return (x.oIndex === iOutline) && (
                        x.pIndex === iProj
                    );
                }, lstProjects);

            return dctProj ? {
                outlineID: dctProj.oID,
                projectID: dctProj.pID,
                path: dctProj.path
            } : undefined;
        }

        // find :: (a -> Bool) -> [a] -> Maybe a
        function find(f, xs) {
            for (var i = 0, lng = xs.length; i < lng; i++) {
                if (f(xs[i])) return xs[i];
            }
            return undefined;
        }

        if (lstProj && (lstProj.length > 0)) {
            var lstMenu = lstProj.map(function (dctProj) {
                    var strProjNum = dctProj.pIndex || ' ';

                    return Array(dctProj.depth + 1)
                        .join('\t') +
                        dctProj.oIndex + '.' + strProjNum +
                        ' ' + dctProj.name;
                }),

                ui = Application("com.apple.systemuiserver"),
                sa = (ui.includeStandardAdditions = true, ui),

                choice = sa.activate() && sa.chooseFromList(lstMenu, {
                    withTitle: strTitle,
                    withPrompt: 'Choose document or project:',
                    defaultItems: lstMenu[0],
                    okButtonName: 'OK',
                    cancelButtonName: 'Cancel',
                    multipleSelectionsAllowed: false,
                    emptySelectionAllowed: true
                });

            return choice ? menuChoiceIDs(
                choice[0], lstProj
            ) : undefined;
        }
    }

    // MAIN
    var ds = Application("TaskPaper")
        .documents,
        d = ds.length ? ds[0] : undefined,

        lstProj = d ? d.evaluate({
            script: projList.toString(),
            withOptions: {
                sortOutlines: true,
                sortProjects: false
            }
        }) : undefined,

        dctProj = lstProj ? projectChoice(lstProj) : undefined;

    return dctProj;
};

#2

Keyboard Maestro version of Jump to any open document or project

The zip includes the Keyboard Maestro macro, and a library script which needs to be saved as:

~/Library/Script Libraries/projectMenu.scpt

jumpToOpenTP3DocOrProj.zip (16.3 KB)


#3

Thanks for a great script @complexpoint

Just to be awkward I was wondering is there any way to filter the list with fuzzy search? Maybe using a variable somehow?

Very impressive what the community is doing with taskpaper.


#4

Not sure – widgets are not a thing I’ve done much of – I wonder if @phil has thoughts about the feasibility/performance of doing that with a Keyboard Maestro HTML dialog – like his date-picker ?


#5

Funny, I was looking into fuzzy searching for a non-TaskPaper project last week. I’ve not had a look at @complexpoint’s macro, so not sure how it could be modified.

My initial thoughts are that this might be best achieved as an Alfred workflow since Alfred has built-in fuzzy matching for its {query} object. This page provides an excellent tutorial for anyone wanting to tackle that. You would essentially want to feed Alfred a list of docs/projects that could be fuzzy searched by Alfred. Selecting one could activate a javascript to take you to that doc/project.

An alternative approach might use this python module called fuzzywuzzy that enables fuzzy matching. You could then somehow present those results via a Keyboard Maestro HTML prompt, but that project is beyond my skill and available time at the moment.


#6

Should that be:

~Library/Scripting Libraries/

I created the one you said and it worked, but’s that is a non-standard folder correct? wasn’t sure where to change it in the script though.

thank you


#7

No need for edits – you’ve already created the correct folder, which is why it’s working. The reference to look at is the section on Script Libraries and that folder in the JXA release notes:

JavaScript for Automation Release Notes


#8

This does not work for me at all

I get undefined when I run the JS

can someone help
thanks


#9

Just trying this out for the first time, and have apparently the same problem as @Nara. Running the script from taskpaper does nothing, running it from script editor just returns “undefined”. I have the projectMenu.scpt in ~/Library/Script Libraries (which I had to create, but I’m assuming that’s not an issue). I’m on 10.12, TP 3.6 Preview.

Any ideas?


#10

I still cannot get it to work :((


#11

I have updated the Application id in the first script to “TaskPaper” (this was a release version change)

That should, I think, be sufficient, but I’ll check tomorrow. (Late in this time-zone now).


#12

ok thanks. I already changed to Taskpaper and I get undefined as the result. I assume you meant projectmenu.scpt