Toggling Multiple Tags with Single Command

I’m sure this must have been done somewhere, but have searched here and on the Keyboard Maestro forums, for instance, and have been unable to quite get this to work. I’m decently versed in programming logics–and have good familiarity with Python, Ruby, AppleScript, etc.–but am a complete dolt when it comes to JavaScript. The language just doesn’t make intuitive sense to me for whatever reason, which I attribute entirely to my failings.

Anyway, put most simply, what I’d like to be able to do is toggle tags, but not just toggle one tag on and off (e.g. run script and add @write if not present on line; remove @write from line if present). Instead, I’d like (for example) to have @write added to line if not present on line, change @write to @edit if run when @write is already tagged to line, and then remove the tag altogether if @edit is already present on line. So, the toggleing would be: off --> @write --> @edit --> off.

I’ve attempted to modify the toggling script I typically use for single tags, but haven’t yet been successful in reliably doing what I want. Any suggestions?

Thanks to everyone in advance for their help!

I think this script does what you want:

function TaskPaperContextScript(editor, options) {
    let outline = editor.outline
    let selection = editor.selection
	
    outline.groupUndoAndChanges(() => {
        editor.selection.selectedItems.forEach((item) => {
			if (item.hasAttribute("data-write")) {
				item.removeAttribute("data-write")			
				item.setAttribute("data-edit", "")			
			} else if (item.hasAttribute("data-edit")) {
				item.removeAttribute("data-edit")						
			} else {
				item.setAttribute("data-write", "")			
			}
        });
	})
	
	editor.moveSelectionToItems(selection)
}

Application("TaskPaper").documents[0].evaluate({
    script: TaskPaperContextScript.toString()
});
2 Likes

This is fantastic, Jesse, and many thanks for your help! One quick follow-up question: is there any way to do the same thing, but for the project containing the currently selected item? For example, changing the status (toggling a status tag that is attached to the entire project) of the project that my cursor is currently inside of?

@jessegrosjean — I love your script! Thank you!

Here is a minor variation that toggles between the following tags:

  • important
  • next
  • today
  • incubate
  • waiting
  • declined

function TaskPaperContextScript(editor, options) {
let outline = editor.outline
let selection = editor.selection

    outline.groupUndoAndChanges(() => {
        editor.selection.selectedItems.forEach((item) => {
			if (item.hasAttribute("data-important")) {
				item.removeAttribute("data-important")		
				item.setAttribute("data-next", "")
			} else if (item.hasAttribute("data-next")) {
				item.removeAttribute("data-next")
				item.setAttribute("data-today", "")
			} else if (item.hasAttribute("data-today")) {
				item.removeAttribute("data-today")
				item.setAttribute("data-incubate", "")
			} else if (item.hasAttribute("data-incubate")) {
				item.removeAttribute("data-incubate")
				item.setAttribute("data-waiting", "")
			} else if (item.hasAttribute("data-waiting")) {
				item.removeAttribute("data-waiting")
				item.setAttribute("data-declined", "")
			} else if (item.hasAttribute("data-declined")) {
				item.removeAttribute("data-declined")
			} else {
				item.setAttribute("data-important", "")
			}
        });
	})

	editor.moveSelectionToItems(selection)
}

Application("TaskPaper").documents[0].evaluate({
    script: TaskPaperContextScript.toString()
});
2 Likes

Instead of processing each selected item you could instead try:

editor.selection.start.parent

That will get you to the parent item of the selected item. Or if you want to get more sophisticated you could search up through ancestors to find project.

and if you wanted to edit the cycle of tags a little more easily, you could experiment with variants like this:

/* eslint-disable max-lines-per-function */
(() => {
    "use strict";

    // Ver 0.02
    // A bit of tidying

    // - TOGGLING SELECTED ITEMS THROUGH A CYCLE OF TAGS -

    // main :: IO ()
    const main = () =>
        Application("TaskPaper").documents.at(0)
        .evaluate({
            script: `${TaskPaperContextScript}`,
            withOptions: {
                tagCycle: [
                    "important",
                    "next",
                    "today",
                    "incubate",
                    "waiting",
                    "declined"
                ]
            }
        });

    // -------------------- TASKPAPER --------------------

    const TaskPaperContextScript = (editor, options) => {

        const tpMain = () => {
            const
                outline = editor.outline,
                selection = editor.selection,
                tagCycle = options.tagCycle;

            outline.groupUndoAndChanges(() =>
                selection.selectedItems.forEach(
                    item => bimap(
                        k => k && item.removeAttribute(
                            `data-${k}`
                        )
                    )(
                        k => k && item.setAttribute(
                            `data-${k}`, ""
                        )
                    )(
                        foundAndNext(tagCycle)(item)
                    )
                )
            );

            editor.moveSelectionToItems(selection);
        };

        // foundAndNext :: [String] ->
        // Item -> (String, String)
        const foundAndNext = tagNames =>
            // A Tuple of the first found tag in the cycle,
            // if any, and the next tag in the cycle, if any.
            item => {
                const
                    iLast = tagNames.length - 1,
                    mbi = tagNames.findIndex(
                        k => item.hasAttribute(`data-${k}`)
                    );

                return 0 <= iLast ? (
                    -1 !== mbi ? (
                        iLast !== mbi ? [
                            tagNames[mbi],
                            tagNames[1 + mbi]
                        ] : [tagNames[iLast], ""]
                    ) : ["", tagNames[0]]
                ) : ["", ""];
            };


        // bimap :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)
        const bimap = f =>
            // Tuple instance of bimap.
            // A tuple of the application of f and g to the
            // first and second values respectively.
            g => ab => [f(ab[0]), g(ab[1])];


        // TaskPaper context main function called.
        return tpMain();
    };

    // Script main function called.
    return main();
})();
2 Likes

Thank you @complexpoint — very informative and inspirational!

1 Like

In case anyone else needs it, I hacked together an example that cycles through tag values, rather than tags themselves. I have very little experience with scripting so if anyone else has a better way to achieve this please let me know :slight_smile:

A GTD status cycle that cycles through next action, later action, waiting action and someday action. If the tag @actionStatus doesn’t exist it’ll create one and default to the start of the cycle (next action).

function TaskPaperContextScript(editor, options) {
    let outline = editor.outline
    let selection = editor.selection
	
    outline.groupUndoAndChanges(() => {
        editor.selection.selectedItems.forEach((item) => {
		if (item.getAttribute("data-actionStatus") === "next action"){
			item.setAttribute("data-actionStatus", "later action")
        } else if (item.getAttribute("data-actionStatus") === "later action"){
			item.setAttribute("data-actionStatus", "waiting action")
        } else if (item.getAttribute("data-actionStatus") === "waiting action"){
			item.setAttribute("data-actionStatus", "someday action")
		} else {
			item.setAttribute("data-actionStatus", "next action")
		}});
	})
	
	editor.moveSelectionToItems(selection)
}

Application("TaskPaper").documents[0].evaluate({
    script: TaskPaperContextScript.toString()
});
3 Likes

Added to wiki

Not sure if anyone can help here - In the above script that I hacked together tags are automatically added to the end of a task line, would anyone know how to change the behavior so that the tag is addede to the beginning of the line instead?

Tags are just parsed out of the plain text item content. The existing TaskPaper API for add/remove tags is in the end just manipulating that plain text. To add a tag to start of line you could do this:

function TaskPaperContext(editor, options) {
	let startItem = editor.selection.startItem
	startItem.bodyString = "@mytag " + startItem.bodyString
}

Application('TaskPaper').documents[0].evaluate({
  script: TaskPaperContext.toString()
});
1 Like

Thanks - is there a way to add the tag between the and the task content? With the sample that you provided the tag is added before the

EDIT: figured it out :slight_smile:

1 Like

An updated version of this script that now includes an emoji for each status of the task:

function TaskPaperContextScript(editor, options) {
    let outline = editor.outline
    let selection = editor.selection
	
    outline.groupUndoAndChanges(() => {
        editor.selection.selectedItems.forEach((item) => {
		
		if (item.getAttribute("data-as") === "now"){
			item.setAttribute("data-as", "next")
			item.bodyContentString = "🟠 " + item.bodyContentString.slice(3)
			
        } else if (item.getAttribute("data-as") === "next"){
			item.setAttribute("data-as", "later")
			item.bodyContentString = "🟡 " + item.bodyContentString.slice(3)
			
        } else if (item.getAttribute("data-as") === "later"){
			item.setAttribute("data-as", "waiting")
			item.bodyContentString = "🟤 " + item.bodyContentString.slice(3)
			
		} else if (item.getAttribute("data-as") === "waiting"){
			item.setAttribute("data-as", "someday")
			item.bodyContentString = "🔵 " + item.bodyContentString.slice(3)


		} else if (item.getAttribute("data-as") === "someday"){
			item.setAttribute("data-as", "now")
			item.bodyContentString = "🔴 " + item.bodyContentString.slice(3)

		} else if (item.getAttribute("data-as") == null){
			item.bodyContentString = "🔴 " + item.bodyContentString + " | "
			item.setAttribute("data-as", "now")

			
		}});
	})
	
	editor.moveSelectionToItems(selection)
}

Application("TaskPaper").documents[0].evaluate({
    script: TaskPaperContextScript.toString()
});
1 Like