Script: Keeping a list of Saved Searches that can be used anywhere


#1

The @search tag approach to saving searches has many advantages, particularly the sidebar listing of available perspectives.

It’s also easy to copy-paste @searches from one document to another (or use a script to gather and paste them)

There are, however, also various ways of keeping all named searches in one place, and using them in any document, without having to paste any @search tags.

As an example, here is a pair of draft scripts

  • searchMENU
  • searchSAVE

searchMENUandSAVE.zip (11.3 KB)

which maintain and use a text file list of named searches, (by default, in the same folder as the scripts).

You could use them with launchers like FastScripts, Keyboard Maestro, etc, but if you simply install them in the TaskPaper Script folder, as described in:

http://guide.taskpaper.com/using_scripts.html

You will see something like:

and once you have used either of them, a text file will appear (with the icon of your default .txt file editor)

You can edit the text file directly by choosing it from the script menu, or test a new view in TaskPaper, and then save it directly, supplying a name for it, using searchSAVE.

searchMENU displays a menu like this:

and applies the chosen search to the current TaskPaper 3 document.

searchSAVE is just a shortcut to adding the currently active filter to the saved menu:

The initial contents of the automatically created text file look like this:

( If you edit it manually, just separate the name from the itemPath with any variant of ->, => or -->, leaving white space to left and right of your arrow)

( and no need to escape parentheses )

Source of searchMENU

// USE A SAVED MENU OF TASKPAPER 3 SEARCHES IN ANY DOCUMENT
// Displays a menu of saved searches for application to any document
// (@search tags not needed)

// Menu saved as a text file in the same folder as the script
// menu format (no backslashes needed for parentheses)
//	name1 -> itemPath1
//  name2 -> itemPath2
// etc.

// Once you have tested a new search in the TaskPaper 3 search panel,
// you can save it to the menu file with a sister script:

// (Other script: SAVE THE ACTIVE TASKPAPER 3 SEARCH (item path filter))

function run() {
    'use strict';

    // OPTIONS
    var dctOptions = {
            'menuFile': 'savedSearches.txt'
        },

        dctDefaultMenu = {
            "completed": "@done",
            "remaining": "(not @done)",
            "projects": "@type=project",
            "top level": "/*",
            "two levels": "/*/*",
            "three levels": "/*/*/*",
            "soon": "@due <[d] now+14 days",
            "overdue": "@due <[d] now"
        }

    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {
        editor.itemPathFilter = options.filter;
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // fileExists :: String -> Bool
    function fileExists(strPath) {
        var fm = $.NSFileManager.defaultManager,
            error = $();
        fm.attributesOfItemAtPathError(
            ObjC.unwrap($(strPath)
                .stringByStandardizingPath),
            error
        );
        return error.code === undefined;
    }

    // readFile :: FilePath -> IO String
    function readFile(strPath) {
        return ObjC.unwrap(
            $.NSString.stringWithContentsOfFileEncodingError(
                strPath, $.NSUTF8StringEncoding, null
            )
        );
    }

    //writeFile :: FilePath -> String -> IO ()
    function writeFile(strPath, strText) {
        var ref = Ref();

        $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                strPath, false,
                $.NSUTF8StringEncoding, ref
            );

        return ref;
    }


    // Menu text file: key '->' value lines (search name '->' search path)
    // deserializeMenu :: String -> {key:value, }
    function deserializeMenu(strLines) {
        return strLines.split(/[\n\r]+/)
            .reduce(function (a, x) {
                var lstParts = x.split(/\s*[-=]+>\s*/);

                if (lstParts.length > 1) {
                    var k = lstParts[0].trim(),
                        v = lstParts[1].trim();

                    if (k && v) a[
                        k.charAt(0)
                        .toUpperCase() + k.slice(1)
                        ] = v;
                }

                return a;
            }, {});
    }

    // Menu text file: key '->' value lines (search name '->' search path)
    // serializeMenu :: {key:value, } -> String
    function serializeMenu(dctMenu) {
        var ks = Object.keys(dctMenu);

        return (
            ks.sort(),
            ks.reduce(function (a, k) {
                return a + k + ' => ' + dctMenu[k] + '\n';
            }, '')
        );
    }

    // MAIN

    var tp3 = Application('com.hogbaysoftware.TaskPaper3'),
        ds = tp3.documents,
        d = ds.length ? ds[0] : undefined;

    if (d) {
        var a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a),

            fm = $.NSFileManager.defaultManager,
            strMenuFile = (dctOptions.menuFile || 'savedSearches.txt'),

            strFolder = ObjC.unwrap(
                $(sa.pathTo(this)
                    .toString())
                .stringByDeletingLastPathComponent
            );


        // Find or create a TEXT FILE listing named searches
        // format:  name -> search
        fm.changeCurrentDirectoryPath(
            strFolder
        );

        if (!fileExists(strMenuFile)) {;
            writeFile(
                strFolder + '/' + strMenuFile,
                serializeMenu(dctDefaultMenu)
            );
        }


        // OFFER A MENU TO THE USER

        var dctMenu = deserializeMenu(readFile(strMenuFile)),
            lstMenu = Object.keys(dctMenu),
            strDelim = '\t\t\t';

        // format as two columns - name and itemPath
        var lngChars = lstMenu.reduce(function (a, k) {
                var lng = k.length;

                return lng > a ? lng : a
            }, 0),
            lst = lstMenu.map(function (k) {
                var strPad = Array(lngChars + 1)
                    .join(' ');

                return (k + strPad)
                    .slice(0, lngChars) + strDelim + dctMenu[k];
            });





        // Use SystemUIServer as the dialog provider
        // in case the script is running from /usr/bin/osascript
        var su = Application("SystemUIServer"),
            sua = (su.includeStandardAdditions = true, su),
            docFile = d.file(),
            strDocName = docFile ? (
                ObjC.unwrap(
                    $(docFile.toString())
                    .stringByAbbreviatingWithTildeInPath
                )
            ) : '(Untitled document ? not yet saved)';

        sua.activate();

        var mbChoice = sua.chooseFromList(lst, {
            withTitle: 'Filter TaskPaper 3 document',
            withPrompt: 'Apply search filter to:\n\n' +
                strDocName + '\n\n' + 'Saved search:',
            defaultItems: lst[0],
            okButtonName: 'OK',
            cancelButtonName: 'Cancel',
            multipleSelectionsAllowed: false,
            emptySelectionAllowed: true
        });

        if (mbChoice) {
            tp3.activate();

            var strChoice = (
                    mbChoice[0]
                    .split(strDelim)[0]
                )
                .trim(),
                strFilter = dctMenu[strChoice];

            d.evaluate({
                script: TaskPaperContext.toString(),
                withOptions: {
                    filter: strFilter
                }
            });

            return sa.displayNotification(
                '=>  ' + strFilter, {
                    withTitle: 'Saved search applied',
                    subtitle: strChoice + ':',
                    soundName: 'default'
                }
            )
        }
    }
}

Source of searchSAVE

// SAVE THE ACTIVE TASKPAPER 3 SEARCH (item path filter)
// adding it to a text file which contains a menu of saved searches,
// and which is used by another script:

// (Other script: USE A SAVED MENU OF TASKPAPER 3 SEARCHES IN ANY DOCUMENT)


// Ver 0.01 
function run() {
    'use strict';

    // OPTIONS
    var dctOptions = {
            // This file is saved, by default, in the same folder as the script
            'menuFile': 'savedSearches.txt'
        },

        dctDefaultMenu = {
            "completed": "@done",
            "remaining": "(not @done)",
            "projects": "@type=project",
            "top level": "/*",
            "two levels": "/*/*",
            "three levels": "/*/*/*",
            "soon": "@due <[d] now+14 days",
            "overdue": "@due <[d] now"
        }

    // TASKPAPER CONTEXT

    function TaskPaperContext(editor) {

        // Read the active filter
        return editor.itemPathFilter;
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // fileExists :: String -> Bool
    function fileExists(strPath) {
        var fm = $.NSFileManager.defaultManager,
            error = $();
        fm.attributesOfItemAtPathError(
            ObjC.unwrap($(strPath)
                .stringByStandardizingPath),
            error
        );
        return error.code === undefined;
    }

    // readFile :: FilePath -> IO String
    function readFile(strPath) {
        return ObjC.unwrap(
            $.NSString.stringWithContentsOfFileEncodingError(
                strPath, $.NSUTF8StringEncoding, null
            )
        );
    }

    //writeFile :: FilePath -> String -> IO ()
    function writeFile(strPath, strText) {
        var ref = Ref();

        $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                strPath, true,
                $.NSUTF8StringEncoding, ref
            );

        return ref;
    }

    // Menu text file: key '->' value lines (search name '->' search path)
    // deserializeMenu :: String -> {key:value, }
    function deserializeMenu(strLines) {
        return strLines.split(/[\n\r]+/)
            .reduce(function (a, x) {
                var lstParts = x.split(/\s*[-=]+>\s*/);

                if (lstParts.length > 1) {
                    var k = lstParts[0].trim(),
                        v = lstParts[1].trim();

                    if (k && v) a[
                        k.charAt(0)
                        .toUpperCase() + k.slice(1)
                        ] = v;
                }

                return a;
            }, {});
    }

    // Menu text file: key '->' value lines (search name '->' search path)
    // serializeMenu :: {key:value, } -> String
    function serializeMenu(dctMenu) {
        var ks = Object.keys(dctMenu);

        return (
            ks.sort(),
            ks.reduce(function (a, k) {
                return a + k + ' => ' + dctMenu[k] + '\n';
            }, '')
        );
    }

    // MAIN

    var tp3 = Application('com.hogbaysoftware.TaskPaper3'),
        ds = tp3.documents,
        d = ds.length ? ds[0] : undefined,

        a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true, a);

    // If a document is open in TaskPaper 3
    if (d) {
        // is there an active itemPath
        var strItemPath = d.evaluate({
            script: TaskPaperContext.toString()
        })

        if (strItemPath.length > 0) {
            var fm = $.NSFileManager.defaultManager,
                strMenuFile = (dctOptions.menuFile || 'savedSearches.txt'),

                strFolder = ObjC.unwrap(
                    $(sa.pathTo(this)
                        .toString())
                    .stringByDeletingLastPathComponent
                ),

                strMenuPath = strFolder + '/' + strMenuFile;

            // Find or create a TEXT FILE listing named searches
            // format:  name -> search
            fm.changeCurrentDirectoryPath(
                strFolder
            );

            if (!fileExists(strMenuFile)) {
                writeFile(
                    strMenuPath,
                    serializeMenu(dctDefaultMenu)
                );
            }

            // OFFER A 'SAVE AS' DIALOG TO THE USER
            var dctMenu = deserializeMenu(readFile(strMenuFile)),
                // using SystemUIServer as the dialog provider
                // in case the script is running from /usr/bin/osascript
                su = Application("SystemUIServer"),
                sua = (su.includeStandardAdditions = true, su);

            sua.activate();

            var dctResult = sua.displayDialog(
                'SAVE:    ' + strItemPath +
                '\n\nadding it to menu file:\n\n' +

                ObjC.unwrap(
                    $(strMenuPath)
                    .stringByAbbreviatingWithTildeInPath

                ) + '\n\nas:', {
                    defaultAnswer: 'name',
                    buttons: ['Cancel', 'Edit menu file', 'Save'],
                    defaultButton: 'Save',
                    cancelButton: 'Cancel',
                    withTitle: 'Save search',
                    withIcon: sua.pathToResource('TaskPaperAppIcon.icns', {
                        inBundle: 'Applications/TaskPaper.app'
                    }),
                    givingUpAfter: 45
                });

            var strName = dctResult.textReturned.trim(),
                blnSave = false;

            if (strName.length > 0) {
                var strUName = strName.charAt(0)
                    .toUpperCase() + strName.slice(1);

                if (dctResult.buttonReturned === 'Save' && dctMenu[strUName]) {
                    var dctConfirm = sua.displayDialog(
                        'Replace existing menu item:\n\n\t' + strUName +
                        '\n\nwith:\n\n\t' +
                        strItemPath, {
                            buttons: ['Cancel', 'OK'],
                            defaultButton: 'Cancel',
                            cancelButton: 'Cancel',
                            withTitle: 'Menu name exists',
                            withIcon: sua.pathToResource(
                                'TaskPaperAppIcon.icns', {
                                    inBundle: 'Applications/TaskPaper.app'
                                }),
                            givingUpAfter: 45
                        })

                    if (dctConfirm.buttonReturned === 'OK') {
                        dctMenu[strUName] = strItemPath;
                        blnSave = true;
                    };

                } else {
                    dctMenu[strUName] = strItemPath;
                    blnSave = (dctResult.buttonReturned === 'Save');
                }

                if (blnSave) {
                    writeFile(
                        strMenuPath,
                        serializeMenu(dctMenu)
                    );

                    sa.displayNotification(
                        '', {
                            withTitle: 'TaskPaper 3 search saved',
                            subtitle: strUName + ' -> ' + strItemPath,
                            soundName: 'default'
                        }
                    )
                }

                if (dctResult.buttonReturned === 'Edit menu file') {
                    var te = Application('TextEdit');
                    te.activate();
                    sa.doShellScript('open "' + strMenuPath + '"')
                }
            }
        } else {
            return sa.displayNotification(
                'TaskPaper 3 saved searches', {
                    withTitle: 'No active search filter',
                    subtitle: '(nothing to save)',
                    soundName: 'default'
                }
            )
        }
    }
}

Saved Searches in Sidebar
#2

I accomplish this using a Keyboard Maestro search palette. Works great.

Sample Taskpaper Search Palette for KM


#3

Good approach, especially for keyboard use.

FWIW, assuming KM 7.1, a JavaScript for Automation version of the script action might look something like:

// TASKPAPER CONTEXT

function fnSetSearch(editor, options) {
    return editor.itemPathFilter = options.query;
}

// JAVASCRIPT FOR AUTOMATION CONTEXT

var tp3 = Application('com.hogbaysoftware.TaskPaper3'),
    ds = tp3.documents,
    d = ds.length ? ds[0] : undefined;

if (d) d.evaluate({
    script: fnSetSearch.toString(),
    withOptions: {
        query: Application('Keyboard Maestro Engine')
            .getvariable('SearchString')
    }
});

(and this Execute JSA action could, if you wanted to be able to adjust the script in just one place, be in a separate macro called by all the menu options:

and, for example:

palette.kmmacros.zip (2.3 KB)


#4

Right on. The Applescript version was a holdover from TP2.

Thanks!


Fold all, unfold all
#5

Thank you for sharing that – good queries, too …


#6

AWWWWW, thanks you guys. Didn’t really use KM for this, but now I am, along with this search pallette and it saves me time with folding everything up nicely since I have quite a few projects. Thanks again!