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?
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?
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();
})();
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
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()
});
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()
});
I’m not sure if anyone here could help me. I have 2x very similar scripts based on the one above. 1x I use for adding @as - action statuses and another for @ps - project statuses.
For some reason if I run the script once on the @as script and then undo (cmd-Z) then Taskpaper crashes. However if I do the same with the @ps script there isn’t an issue (i.e. no crashing).
The last of those conditional clauses is the one that I would look at first – it shows signs of editing at a different time or by a different hand. ( Using == where in practice, like the other clauses, we should always use the faster and more type-safe ===. == performs type conversions which can have slippery and confusing consequences)
Not quite sure what the role of the "|" pipe character is, but I’m also wondering what happens with that clause when the selection includes empty lines.
(first step might be to simply remove that final clause, and see if the script still gets the TaskPaper model into a fragile place with your test data)
Having tidied it slightly with ESlint (just to make it easier for me to read, and to get the benefits of the more informative “use strict”; error messages), and having also pruned out some redundant logic and a number of redundant data-fetches, I get the edit below, with which I haven’t yet been able to reproduce your crash.
Do you have some simple sample data with which it’s reproducible ?
Expand disclosure triangle to view ESLint-tidied Source
Incidentally, It might not be too difficult to imagine contexts in which that slice(3) might be discarding initial characters which turned out not be billiard ball + space after all.
Probably sensible to include a prior check – I suppose a start-of-line regex might be one approach. It would need the /u switch to cope with Emoji characters.
The last clause is meant to catch tasks that don’t have an @as tag at all, and because they don’t have that tag, I assume that they also don’t have the pipe character or the icon / emoji. That’s also why I omit the slice in the last clause.
I tried also removing the pipe character and it also crashes. It’s only when I remove the space character and pipe does it not crash i.e.
I tried yet another option and found that if the value passed to item.bodyContentString ends in a space character that’s when the crash happens. So the following works:
item.bodyContentString = `🔴 ${bcs} |`;
Lastly, I tried this on my original (way less elegant script) and found that if I removed the last space character from it, it too didn’t crash i.e. changing " | " to " |"
Interesting enough in all the cases that I don’t add the space character and in turn Taskpaper doesn’t crash, a space character is added by Taskpaper itself
FYI as a test all I use is a new Taskpaper file with the following:
The last clause is meant to catch tasks that don’t have an @as tag at all,
There’s no need to catch them – if a line lacks {now, next, later, waiting} then its simply now
(and I think you may be inadvertently creating a tangle for cases where the line is blank – especially with the type-converting == which you have left in there in lieu of the safer === which I mentioned)