Combining TaskPaper with 'Tyme' (for time session tracking)?


#1

Is anyone using a scripted (or other) solution for time-tracking with their TaskPaper projects ?

I notice that Tyme is scriptable, allowing you to attach one or more timeSessions to each task of a project (and yielding project time summations, with time*hourly_rate, and proportional time allocation stats for billing and overview etc).

It looks like it might work fairly well as a scripted time-tracking adjunct to working through projects in TaskPaper.

I am planning to draft and test an experimental script or two, but do let me know if you are already using some kind of time-tracking workflow with TaskPaper, and have any ideas of which parts might most usefully be automated from TP using Tyme or some other similar tool.


#2

I’m not, but looks like a nice project.


#3

Way back in the day I had something setup with TicToc, but they’ve discontinued that and I no longer had a need for continuous and rigorous time tracking. Time however does look interesting and I’ve been keeping an eye on the apps.


#4

Here’s a very rough draft of a core script which simply toggles tracking in Tyme on and off for the task selected in TaskPaper 3

(If it doesn’t find an existing match for the selected task (and/or its parent project) in Tyme, a new project and/or task is created in Tyme using the TaskPaper 3 bodyStrings for each)

// Draft 0.0.5

// Changes 0.0.5  updated for API change: .bodyDisplayString -> .bodyContentString
// Changes: 0.0.4 updated for TaskPaper 3.0 build 167 (editor.selection.startitem)
// Changes: 0.0.3
// 	- Text of name used for tasks and projects in Tyme now excludes any TP tags
// 	- Simple TaskPaper tags (no parentheses) on task or their ancestors up to
//    (but not including) the enclosing project are pass through as Tyme 'taskTags'
//  - The **values** of @account( ...) or @ref( ... ) task on the project line or its path
//    are passed through as Tyme 'projectTags'
//    (To use other tag names for the two levels of project tags, change the values of the
//    strAccountTag and strRefTag arguments in the last line of the script.


// 1. Install http://tyme-app.com/mac
// 2. In TaskPaper 3, select a task in some project,
//    and run this (El Capitan JavaScript for Automation) draft script to test
//    the toggling of time-tracking for the selected task.

//		If the selected task is being tracked, tracking stops.

//	    If the selected task is NOT being tracked:
//			a) tracking of any other task ends, and
//			b) tracking of this task begins.

//		(Each toggle event adds/completes a new timeSession record for the task in Tyme.app)



// ADJUST THE VALUES OF THESE TWO ARGUMENTS IN THE LAST LINE OF THE SCRIPT
(function (strAccountTag, strRefTag) {
	'use strict';

	strAccountTag = strAccountTag || 'account';
	strRefTag = strRefTag || 'ref';

	// ACCOUNT TAG
	// e.g. if something like @ref(BBC) @ref(FT) @ref(NYT) is on any ancestor
	// of the timed task in TP, the value of the @ref tag (eg. 'BBC' 'FT' etc)
	// will be passed through to Tyme as a tag which can be used to aggregate billing and reporting


	// FUNCTION EVALUATED IN THE TASKPAPER (rather than JSA) CONTEXT:
	// () -> MayBe {projectName:String, taskName:String, projectTags:[String], taskTags:[String]}
	function fnTPSeln(editor, options) {

		// value of any instanceof this tag on this node or its path ?
		function ancestralTagValue(node, strTag) {
			var lstHits = node.evaluateItemPath(
				'(ancestor-or-self::@' + strTag + ')[-1]'
			);

			return lstHits.length > 0 ? (
				[lstHits[0].attributes['data-' + strTag]]
			) : [];
		}

		// all map results flattened down to a single list
		// [a] -> (a -> b) -> [b]
		function concatMap(xs, f) {
			return [].concat.apply([], xs.map(f));
		}

		// all simple no-parentheses tags on this task and/or path up to
		// (but excluding) the project of a task
		// TP3Node -> [String]
		function simpleTaskTags(nodeTask) {
			return concatMap(
				nodeTask.evaluateItemPath(
					'ancestor-or-self::* except ancestor::@type=project/ancestor-or-self::*'
				),
				function (item) {
					var dctAttribs = item.attributes;

					return Object.keys(dctAttribs).filter(function (k) {
						return dctAttribs[k] === '';
					}).map(function (k) {
						return k.slice(5);
					});
				}
			);
		}

		// Up to two project tags e.g. for Account and Refce
		// TP3Node -> String -> String -> [String]
		function projectTags(prj, strAccountTag, strRefTag) {
			return (strAccountTag ? ancestralTagValue(prj, strAccountTag) : []).concat(
				strRefTag ? ancestralTagValue(prj, strRefTag) : []
			);
		}

		// MAIN (for TaskPaper context, rather than JavaScript for Automation)
		// () -> MayBe {projectName:String, taskName:String, projectTags:[String], taskTags:[String]}
		var rng = editor.selection,
			sln = rng ? rng.startItem : null,

			// project containing selection ?
			prjs = sln ? sln.evaluateItemPath(
				'(ancestor::@type=project)[-1]'
			) : [],
			prj = prjs.length > 0 ? prjs[0] : undefined;

		// project and task strings ?
		return sln && prj ? {
			project: prj.bodyContentString,
			task: sln.bodyContentString,

			projectTags: projectTags(prj, options.accountTag, options.refTag),
			taskTags: simpleTaskTags(sln)
		} : undefined;
	}


	// FUNCTIONS FOR OSASCRIPT JS CONTEXT

	// (AUTOMATING TYME.APP with TASKPAPER DATA)

	// Found or created Project, Task, or ProjectTag or TaskTag
	// Application -> Object -> String -> String -> Object
	function foundOrCreatedByName(app, oParent, strClass, strName) {
		var elements = oParent[
				strClass.charAt(0).toLowerCase() + strClass.slice(1) + 's'
				],
			iElem = elements.name().indexOf(strName);

		// Found or, if not found, newly created
		return iElem !== -1 ? elements.at(iElem) : (function () {
			var elem = app[strClass]({
				name: strName
			});
			elements.push(elem);
			return elem;
		})();
	}

	// Found or created (and tagged) Project or Task
	// Application -> Object -> String -> String -> [String] -> Object
	function taggedFoundOrCreatedByName(app, oParent, strClass, strName, lstTags) {
		var oElem = foundOrCreatedByName(app, oParent, strClass, strName),
			strTagClass = strClass + 'tag';

		lstTags.forEach(function (strTag) {
			// Find or create a project or task tag
			foundOrCreatedByName(
				app, oElem,
				strTagClass,
				strTag
			);
		});

		return oElem;
	}


	// MAIN (for osascript JS context)

	// From TaskPaper: {projectName:String, taskName:String, projectTags:[String], taskTags:[String]}
	var ds = Application('com.hogbaysoftware.TaskPaper3').documents,
		dctProjTask = ds.length ? ds[0].evaluate({
			script: fnTPSeln.toString(),
			withOptions: {
				accountTag: strAccountTag,
				refTag: strRefTag
			}
		}) : undefined;

	// Assuming that a task in a project outline was selected ...
	if (dctProjTask && dctProjTask.project && dctProjTask.task) {

		var tyme = Application('Tyme'),
			oTask = taggedFoundOrCreatedByName(
				tyme,
				taggedFoundOrCreatedByName(
					tyme, tyme,
					'Project', dctProjTask.project,
					dctProjTask.projectTags
				),
				'Task', dctProjTask.task,
				dctProjTask.taskTags
			);

		tyme.activate();

		// TOGGLE TRACKING OF THE TASK IN TYME.APP
		var oTracked = tyme.trackedtask,
			strID = oTracked.id();

		if (strID) {
			tyme.delete(oTracked);
			if (strID !== oTask.id()) tyme.trackedtask = oTask;
		} else tyme.trackedtask = oTask;

	} else {
		var a = Application.currentApplication(),
			sa = (a.includeStandardAdditions = true, a);

		sa.activate;
		sa.displayDialog(
			[
				'http://www.taskpaper.com/',
				'http://tyme-app.com/mac/',
				'',
				'Select a TaskPaper task within a project outline ...'
			].join('\n'), {
				withTitle: 'Session timing from TaskPaper 3 with Tyme',
				buttons: ['OK'],
				defaultButton: 'OK',
				givingUpAfter: 20
			});

	}

	return dctProjTask;

})('account', 'ref'); // enter alterative Referencing tag. e.g.  'bill' for @bill(BBC)


Script: Copying Tyme.app projects and tasks to TaskPaper
#5

Ver 0.0.3 above:

Changes:

  • The text of the name used for tasks and projects in Tyme now excludes any TP tags
  • Any simple TaskPaper tags (no parentheses) on the selected task or its ancestors up to
    (but not including) the enclosing project, are passed through as Tyme taskTags
  • The values of any @account() and/or @ref() tag on the project line (or anywhere on its ancestral path)
    are passed through as Tyme projectTags

Tyme.app allows filtering of time-tracking stats by project tags or task tags.
This script assumes that any TaskPaper project will relate to just one account/client and just one invoicing reference, but if that doesn’t match your workflow, it can be easily adjusted.

(To use other tag names for the two levels of project tags (rather than @account() and @ref()), change the values of the strAccountTag and strRefTag arguments in the last line of the script).


#6

Have you tested against Taskpaper 3? I’m getting “application isn’t running” when I attempt to execute the script after selecting the task.


#7

Yes, this is TaskPaper 3 only.

You have Tyme.app installed ?


#8

If there is a clash on your system between two different versions of TaskPaper, it might be worth editing the JavaScript:

from

Application("TaskPaper")

to

Application("com.hogbaysoftware.TaskPaper3")

#9

Yes, both apps are open. It is dying on line 157.

I also did the change above in case I had some old bits around but it dies at the same point.


#10

Things I would check:

  • in Script Editor: Application('com.hogbaysoftware.TaskPaper3').documents.length

you should see something like this:

  • Your operating system version (I’m using OX X 10.11.2 here)
  • Whether you have a fresh copy of the whole script text

#11

ok, after getting the same error with the above test script. I completely restarted everything and it finally started working. Not sure if I still had an old version laying around or what but it is functioning now.

Thanks for the help.


#12

Let me know if you notice wanting any additional automation in it. 2 things that have occurred to me are:

  • Sending the value of any TaskPaper @due(yyyy-mm-dd [HH:MM) tag (in the selected line or its project) to Tyme
  • Combining @done tagging in TaskPaper with stopping any corresponding timeSession in Tyme

#13

Updated (above) to ver 0.0.4 for compatibility with TaskPaper 3 build 167

(Slight adjustment to the scripting interface to selections)


#14

Updated to 0.0.5 for compatibility with the release version of the TaskPaper 3 API

.bodyDisplayString -> .bodyContentString

Planning take a look at Tyme 2 (which has new API) over the weekend.

So now a good time to ask me if anyone has special requests or puzzlements :slight_smile:


#15

Just wanted to say thanks for doing this - really appreciated. (So useful that I’ve gone back to Tyme 1!!)

Look forward to the Tyme2 updates :slight_smile:


#16

I have tried to adapt the script for Tyme 2, and scripted project creation works in it, but scripted creation of tasks appears to be broken.

I’ll give an update if the maker is able to fix or shed some light on that for Tyme 2.

UPDATE

Got a very quick and helpful response – not broken – the type constructor just needs an extra parameter now – I’ll do something at the weekend.


#17

Much appreciated :bow:


#18

Here is a first draft of a Tyme 2 version which simply toggles tracking for the item selected in the front document of TaskPaper 3.

Would you like to test it ?

Tyme 2 allows us to assign categories (clients etc) to projects, but the scripting interface doesn’t seem to give us access to that (yet ?) I would really like to get it to pick up a client tag from TaskPaper 3, and find/create the project in a matching Tyme2 category, but I think that may be out of reach, at least for the moment.

Seems to be working on my system OSX 10.11, TaskPaper 3.3 (211), Tyme2 (Version 1.3.2 (1850))

// Ver 0.003

// 0.002 stops tracking of any other task before starting tracking for this one
//          (rather than simply toggling tracking for the selected task only)
// 0.003 Updated to be unaffected by changes in TaskPaper app id

// Toggling Tyme2 tracking for the item selected in TaskPaper 3's front document

(function (dctOptions) {

    // TASKPAPER CONTEXT
    function TaskPaperContext(editor, options) {
        var item = editor
            .selection
            .startItem;

        var projects = editor.outline
            .evaluateItemPath('ancestor::project[-1]', item);

        return {
            project: projects.length ? (
                projects[0].bodyContentString
            ) : (options.defaultProject || 'Miscellaneous'),
            task: item ? item.bodyContentString : undefined
        }
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // Found or created item in a collection exposed by an app
    // E.g. a project or a task in Tyme2

    // Object -> Object -> String -> Object -> Object
    function foundOrCreated(app, oParent, strClass, dctProps) {
        'use strict';

        // elements
        var elements = oParent[
                strClass.charAt(0)
                .toLowerCase() + strClass.substr(1) + 's'
            ],

            // dctProps -> lstTests
            lstTests = Object.keys(dctProps)
            .map(function (k) {
                var dct = {};
                return dct[k] = dctProps[k], dct;
            }, {}),

            // found or created
            found = elements.whose(
                (lstTests.length > 1 ? {
                    _and: lstTests
                } : lstTests[0])
            ),

            blnFound = found.length > 0,
            elem = (blnFound ? found()[0] : app[strClass](dctProps));

        // pushed into collection if new
        return (blnFound || elements.push(elem)), elem;
    }


    var ds = Application('TaskPaper')
        .documents,

        dctTP3Seln = ds.length ? ds[0].evaluate({
            script: TaskPaperContext.toString(),
            withOptions: dctOptions
        }) : undefined;


    if (dctTP3Seln && dctTP3Seln.task) {
        // TYME 2
        var Tyme2 = Application('de.lgerckens.Tyme2'),

            oTask = foundOrCreated(
                Tyme2,
                foundOrCreated(
                    Tyme2, Tyme2,
                    "Project", {
                        name: dctTP3Seln.project
                    }
                ),
                "Task", {
                    name: dctTP3Seln.task,
                    tasktype: 'timed'
                }
            ),
            
            // Toggle tracking
            strID = oTask.id(),
            lstTracking = Tyme2.trackedtaskids(),
            blnTracking = lstTracking.length && lstTracking
            .indexOf(strID) !== -1;

        // pause all existing tracking
        lstTracking.forEach(function (x) {
            return Tyme2.stoptrackerfortaskid(x);
        });

        // and start tracking for this item if required
        if (!blnTracking) {
            Tyme2.starttrackerfortaskid(strID)
        }
    }

})({
    defaultProject: 'Miscellaneous' // for tasks not enclosed by a TP3 project
});

#19

Thanks for this !

Quick update as testing now - OSX 10.11.4 (15E65), Taskpaper Version 3.2 (200), Tyme 2 1.3.1 (1841)

  • Works well - toggling timer off and on for existing tasks within existing projects
  • Also works well for new projects under Miscellaneous
  • Does create a new timed entry for new task under existing project, but for some reason doesn’t show as a new task under that project on the projects view in Tyme. Does show on the Time entries screen and turns up on the Archive screen although not marked as completed.

Will carry on testing later and also delete my archive - see what that does. :slight_smile:

UPDATE

Now works perfectly - I had some weird archive conflict - prob. from running Tyme 1 and Tyme 2


#20

Thanks for checking it !