A general date-picker script for any tag type

Following the theme of @Phil’s earlier @start date picker, here is a draft of a general date-picker script for TaskPaper 3.

pickDateTime.app.zip (52.7 KB)

USE:

  • Adds or updates a datetime (for any @tag containing or to the left of the cursor)
  • Adds the specified default tag to lines in which no tag has yet been typed.

To add a date:

  • type the first part of a tag, e.g. @due (no need for parentheses)
  • launch the script and pick a date from the calendar

To alter a date:

  • place the selection cursor in or to the right of the dated tag
  • launch the script and pick a different date

(The clock is mainly for display of any existing time. You can drag the hands to change the time if you want to, but I personally prefer to change the time part of tags manually. The date picker is mainly useful for the calendar and the days of the week)

(Doesn’t need any particular launcher app like Keyboard Maestro, LaunchBar etc, though it can, of course, be used with any of them).

INSTALLATION:

The JavaScript for Automation source (below) needs to be saved from Script Editor as an .app file (rather than .scpt),

Save As > File Format > Application

with: ‘‘Stay open after run handler’’ checked

and then run from the Script Menu in TaskPaper

As described here: http://guide.taskpaper.com/using_scripts.html

Once the .app file is in the ~/Library/Scripts/Applications/TaskPaper folder, it can be run not only from the TaskPaper 3 script menu:

but also from any launcher like LaunchBar (add ~/Library/Scripts/Applications/TaskPaper to the LaunchBar index) or Keyboard Maestro

CUSTOMIZATION:

There are two options which can be adjusted at the end of the script:

  • The tag to add if the selected line does not yet have any tags
  • the default time to use when creating date tags (default is ‘00:00’ which results in no time being displayed in the tag)
{
    defaultTag: 'start', // if no existing tag  (without '@' or 'data-' prefix)
    defaultTime: '00:00' // if no existing time (24:00 format)
}

JavaScript for Application source:

(function (dctOptions) {
    'use strict';


    // Ver 0.2  (displays text of selected line in dialog)

    // TASKPAPER CONTEXT

    // selectedTag :: editor -> maybe String
    function selectedTag(editor) {
		'use strict'
		
        // tagPositions :: item -> [{location: Int, tagname: String}]
        function tagPositions(item) {
            var s = item.bodyHighlightedAttributedString;

            for (var lng = s.length, rng = {
                    location: 0
                }, i = 0, lst = [], dct; i < lng;) {

                dct = s.getAttributesAtIndex(i, rng);

                if (dct.tagname) {
                    lst.push({
                        tagname: dct.tagname,
                        location: rng.location
                    });
                }
                i += rng.length;
            }
            return lst;
        }

        // 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;
        }

        // reverse :: [a] -> [a]
        function reverse(xs) {
            var sx = xs.slice(0);
            return sx.length > 1 ? sx.reverse() : sx;
        }

        // List the positions of all the tags in this item
        var selection = editor.selection,
            item = selection.startItem,
            strType = item.getAttribute('data-type'),
            strExceptTags = (strType === 'task' ? '- ' : '') +
            item.bodyContentString + (strType === 'project' ? ':' : ''),
            iSelnOffset = selection.end - editor.getLocationForItemOffset(
                item, 0
            ),
            lstTagPosns = tagPositions(item);

        // work back from the end of the selection to the first tag
        // which starts before that location
        if (lstTagPosns.length > 0) {
            var maybeTag = find(function (x) {
                return (x.location < iSelnOffset);
            }, reverse(lstTagPosns));



            return {
                content: strExceptTags,
                tag: maybeTag ? {
                    tag: maybeTag.tagname,
                    dateTime: item.getAttribute(
                        maybeTag.tagname,
                        Date
                    ) || undefined
                } : undefined
            }
        } else return {
            content: strExceptTags,
            tag: undefined
        };
    }

    // Set a tag/value pair in the first item of the selection
    function setTagDate(editor, options) {
        var strTag = options.tag,
            dteDate = options.when,
            item = editor.selection.startItem;

        if (strTag && dteDate && item) {
            item.setAttribute(
                strTag,
                DateTime.format(dteDate)
            );
        }
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    ObjC.import('AppKit');
    ObjC.import("Cocoa");

    // datePicker :: $.NSDate -> $.NSDatePicker
    function datePicker(initialDate) {
        var oPicker = $.NSDatePicker.alloc.initWithFrame(
            $.NSMakeRect(0, 0, 100, 100)
        );

        oPicker.setDatePickerStyle(
            $.NSClockAndCalendarDatePickerStyle
        );
        oPicker.setDatePickerElements(
            $.NSYearMonthDayDatePickerElementFlag |
            $.NSHourMinuteDatePickerElementFlag
        );
        oPicker.setDateValue(initialDate);

        return oPicker;
    }

    // pickerView :: $.NSDatePicker -> ($.NSView, $.NSDatePicker)
    function pickerView(oPicker) {
        var dctSize = oPicker.fittingSize,
            oView = $.NSView.alloc.initWithFrame(
                $.NSMakeRect(0, 0, 100, 200)
            );

        oView.setFrameSize(dctSize);
        oPicker.setFrameSize(dctSize);
        oView.setSubviews($.NSArray.arrayWithObjects(oPicker));

        return {
            picker: oPicker,
            view: oView
        };
    }

    // pickerAlert :: $.NSView -> $NSDatePicker String -> String -> maybe JS Date
    function alertResult(dctPickerView, strVerb, strTagName, strText) {
        var oAlert = $.NSAlert.alloc.init;

        oAlert.setMessageText(strVerb + ' @' + strTagName +
            ' tag in TaskPaper 3');
        oAlert.setInformativeText(
            strText
        );
        oAlert.addButtonWithTitle('OK');
        oAlert.addButtonWithTitle('Cancel');
        oAlert.setAccessoryView(dctPickerView.view);
        oAlert.icon = $.NSImage.alloc.initByReferencingFile(sa.pathToResource(
                'TaskPaperAppIcon.icns', {
                    inBundle: 'Applications/TaskPaper.app'
                })
            .toString());

        return (oAlert.runModal === $.NSAlertSecondButtonReturn) ? (
            undefined
        ) : ObjC.unwrap(dctPickerView.picker.dateValue);
    }

    // MAIN

    // 1. Selected tag ? With date-time value ?

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

    if (d) {
        var dctSelected = d.evaluate({
                script: selectedTag.toString()
            }),
            dctTagDate = dctSelected.tag || {
                tag: 'data-' + dctOptions.defaultTag
            },
            strDate = dctTagDate.dateTime,

            // Existing date and time, or today, with time set to default 
            // (e.g. 00:00 see dctOptions.defaultTime below)
            // if not specified
            dteInitial = strDate ? new Date(strDate) : (function () {
                var dte = new Date(),
                    lstTime = dctOptions.defaultTime.split(':');

                if (lstTime.length > 1) {
                    dte.setHours(
                        isNaN(lstTime[0]) ? 0 : parseInt(
                            lstTime[0], 10
                        ),
                        isNaN(lstTime[1]) ? 0 : parseInt(
                            lstTime[1], 10
                        ), 0, 0
                    )
                } else dte.setHours(0, 0, 0, 0);

                return dte;
            })();


        // 2. User response to date picker showing default or selected tag
        //         with default or selected time
        var a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        a.activate();



        var maybeDate = alertResult(
            pickerView(
                datePicker(dteInitial)
            ),
            dctSelected ? (
                (strDate ? 'Update' : 'Choose date for')
            ) : 'Create',
            dctTagDate.tag.substr(5),
            dctSelected.content
        );

        // 3. If user response, updated tag

        if (maybeDate) {
            d.evaluate({
                script: setTagDate.toString(),
                withOptions: {
                    tag: dctTagDate.tag,
                    when: maybeDate.toString()
                }
            });

            tp3.activate();
        }
        sa.quit(); // Close the date-picker applet
    }

})({
    defaultTag: 'start', // if no existing tag  (without '@' or 'data-' prefix)
    defaultTime: '00:00' // if no existing time (24:00 format)
});
4 Likes

Here is a version which should work with the 3.5 Preview,

pickDateTime35.app.zip (53.0 KB)

if launched from the TaskPaper script menu:

Note that sandboxing prevents this kind of app bundle (needed to use the date picker control) from being launched from the TaskPaper command palette.

1 Like

I got a version working from inside the command palette. I’m calling an applescript to run the application. So, there’s 2 files in the zip, both should go inside “~/Library/Application Scripts/com.hogbaysoftware.TaskPaper3.direct”.

pickDateTime-ScriptApp.zip (57.3 KB)

1 Like

hmm, I get an error for line 165, “Application not found”? :frowning:

Try this one, should work now

pickDateTime-ScriptApp-2.zip (57.1 KB)

like a champ! thank you!

Thanks - you did well to try opening the .app path from AppleScript – I had only tried doing it from JSA, and curiously, that triggers a privilege violation error which using AS seems to bypass …

A bit unexpected, and worth knowing

I tried at first with JXA as well since the script is built with it. However, it was my first time playing with it and I’m more used to Applescript. So I tested with the later and got it to work. However, I just tried again with JXA and it’s working fine as well! Actually, I think it’s even better then the one in Applescript (seems a little faster). So, if anyone want to have a look or use this on, here it is:

pickDateTime-ScriptApp-JXA.zip (57.1 KB)

I got it to work in the global context and not inside the TaskPaper context, so maybe that’s the difference from what you tried.

Also, at first, I wanted to make it work directly with the script and I almost got it, but I can’t set the focus to the Alert dialog… So, if you want to have a look, what I changed from your script to make it pop in front is to remove the “a.activate()” and the “sa.quit()”. I know it’s with “a.activate()” that should put the focus on the alert dialog, however, it put it behind… But, I think it’s best as an application since if the dialog get hidden behind a window, it’s hard to get it back and we need to close it from the Activity Monitor.

Last thing, I wanted to say kudos on the script! I’m on El Capitan since not too long (was still on Snow Leopard) and that gave me a look at some great things that we can do with JXA! Thanks!

1 Like

Even better :slight_smile:

The approach that triggers privilege violation in JXA but not AppleScript is using a .doShellScript() open filePath line.

SKIP THIS: (works from Script Editor but not from the TaskPaper 3 command palette)

function run() {
    'use strict';

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

        strPath = $(
            '~/Library/Application\ Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app'
        )
        .stringByStandardizingPath.js;
    
        sa.doShellScript('open "' + strPath + '"');
}

Your approach of using a direct Application(appPath) reference hadn’t even occurred to me, and is working well here too.

Global vs module context does indeed also seem to make a difference. Learning a lot :slight_smile:

Many thanks.

PS another thing which we should perhaps check, before we make a list of things which do and don’t work when launching from the Command Palette type of context, is whether or not ObjC $() calls like

        strPath = $(
            '~/Library/Application\ Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app'
        )
        .stringByStandardizingPath.js;

are OK.

(Can’t do that this morning, but let me know if you find out …)

DRAFT SUMMARY

If you want to call an .app-bundle script from TaskPaper 3 > Palette > Command

The global context pattern works in JavaScript for Automation, including with ObjC calls:

Works

Application(
    $(
        '~/Library/Application Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app'
    )
    .stringByStandardizingPath.js
);

But the module pattern, and the function run() {} and .doShellScript patterns (which do work for this purpose in the Script Editor context), do not seem to be appropriate or usable from the Command Palette context.

Module pattern not usable for .app-bundle script launching from palette

// fails
(function () {
    'use strict';

    Application(
        $(
            '~/Library/Application Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app'
        )
        .stringByStandardizingPath.js
    );

})();

function run() pattern not usable for .app script launching from palette

// fails
function run () {

    Application(
        $(
            '~/Library/Application Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app'
        )
        .stringByStandardizingPath.js
    );
}

.doShellScript pattern not usable for .app script launching from palette in JS (OK from AS)

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

    var strCmd = 'open "' + sa.pathTo('home folder') + '/Library/Application Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app"';
    
    sa.doShellScript(strCmd);

At least this appears to be the picture on the (10.12) system here …

Hey, I guess we’re not in the same timezone! You were faster then me this morning :wink: So, basically, yes what you mentioned works great here as well! To expand on the list of working examples:

In Javascript for Automation (Basically the same as yours but by using pathTo):

var app = Application.currentApplication()
app.includeStandardAdditions = true

Application(app.pathTo("home folder") + "/Library/Application Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app");

In AppleScript:

do shell script "open -a ~/Library/Application\\ Scripts/com.hogbaysoftware.TaskPaper3.direct/pickDateTime.app"

Applescript version 2 (But the there’s something that seems to be called when the application quit, so the try / end try block prevent the error – so not the best option):

try
	set appPath to ((path to home folder as string) & "Library:Application Scripts:com.hogbaysoftware.TaskPaper3.direct:pickDateTime.app")
	tell application appPath to activate
end try

Hey!

Thank you so much for providing this script - but I do have a problem with it:

the dialog pops up and initializes with the date of an existing tag, but when I select a new date and click “OK” the tag get’s updates with the text “invalidDate” for the new date.
From what I can see it seems as if maybeDate.toString() is returning an invalid date - anybody else having this problem??

Sorry I can’t answer questions about this script, but please note that it was written before TaskPaper started including a built in date picker. You might be able to use the internal date picker to solve your needs. Here’s a quick demo:

Hi Jesse,

Thank you for the quick response!
The functionality I’m looking for is editing an existing date, for example in a due tag.
Is there a way to bring up the built in date picker to edit an existing date?

Thanks!