Hello all,
I am looking to create a JavaScript that will place the cursor at the end of the first task that is not @done
I don’t want to focus in—just move the cursor.
Any ideas are appreciated.
Thanks!
Jim
Hello all,
I am looking to create a JavaScript that will place the cursor at the end of the first task that is not @done
I don’t want to focus in—just move the cursor.
Any ideas are appreciated.
Thanks!
Jim
You may find some bits of it in this draft:
(() => {
"use strict";
// Rob Trew @2021
// ------------------- JXA CONTEXT -------------------
// jxaMain :: IO ()
const jxaMain = () => {
const docs = Application("TaskPaper").documents;
return 0 < docs.length ? (
docs.at(0).evaluate({
script: `${TaskPaperContext}`,
withOptions: {
itemPath: "//not @done"
}
})
) : "No documents open in TaskPaper";
};
// ---------------- TASKPAPER CONTEXT ----------------
// TaskPaperContext :: Editor -> Dict -> IO ()
const TaskPaperContext = (editor, options) => {
const cursorToEndOfItem = item => (
editor.moveSelectionToItems(
item, item.bodyString.length
),
`Cursor at ${editor.selection.location}`
);
const
outline = editor.outline,
matches = outline.evaluateItemPath(
options.itemPath
)
// Perhaps ignoring blank lines ?
.filter(
item => Boolean(item.bodyString.trim())
);
return 0 < matches.length ? (
cursorToEndOfItem(matches[0]),
`Cursor at ${editor.selection.location}`
) : `No matches for '${options.itemPath}'`;
};
return jxaMain();
})();
Excellent @complexpoint — In early tests, I cannot find any issues in your draft. Thank you!
If you do want to exclude blank lines, another approach (as an alternative to the .filter
above), might be to specify non-blank lines in the itemPath
.
@jessegrosjean might be able to think of a more elegant expression, but this at least seems to work:
//not @done and @text matches "."
(i.e. a regular expression that requires at least one character in the line)
I presume that the quotes around the period need to be escaped, right?
Example:
itemPath: "//not @done and @text matches \"@\""
This is really good @complexpoint !
Experimenting with:
itemPath: "//not @done and @text matches today"
and
itemPath: "//not @done and @text matches next"
That’s right – quoting allows, for example, for the inclusion of white space in a regex.
JS gives you a couple of options there for the outermost quotes. You could do without escaped inner quote by writing, perhaps:
'//not @done and @text matches "."'
(footnote:: JSON needs double-quoted strings, but within full JS code the notation for a key or string value in a Dictionary/Object, or just a plain string, can use single quotes (or for template notation, back-ticks)
Note that:
matches
keyword is only really needed if the following string contains some regex syntax@text
value anywaySo if you wanted, I think you could pare the itemPath
pattern down to things like:
//not @done and today
The above has served me well for years—thank you again @complexpoint !
I have now found a desire to move the cursor to the end of the first task that is not @done and is in the project that the cursor is currently in.
In this example, the cursor is somewhere in Project B (or in one of its children). After the script is run, the cursor would be at the end of Task 3b
Project A:
Task 1a
Project B:
Task 1b @done
Task 2b @done
Task 3b
If this desire easy to implement in the above script? Or would this be a major rewrite?
Here’s a rough edit.
(messages could be improved – for example when the cursor is not enclosed by any project, or the enclosing project’s tasks are all marked @done
)
(() => {
"use strict";
// Rob Trew @2021, 2024
// First not @done selected in project containing cursor
// ------------------- JXA CONTEXT -------------------
// main :: IO ()
const main = () => {
const doc = Application("TaskPaper").documents.at(0);
return doc.exists()
? doc.evaluate({
script: `${TaskPaperContext}`
})
: "No documents open in TaskPaper";
};
// ---------------- TASKPAPER CONTEXT ----------------
// TaskPaperContext :: Editor -> IO ()
const TaskPaperContext = editor => {
const cursorToEndOfItem = item => (
editor.moveSelectionToItems(
item, item.bodyString.length
),
`Cursor at ${editor.selection.location}`
);
const
cursorID = editor.selection.startItem.id,
itemPath = (
`//@id=${cursorID}/ancestor-or-self::project//task not @done[0]`
),
matches = editor.outline.evaluateItemPath(itemPath);
return 0 < matches.length
? (
cursorToEndOfItem(matches[0]),
`Cursor at ${editor.selection.location}`
)
: `No matches for '${itemPath}'`;
};
// MAIN ---
return main();
})();
Solid, with one desire: if the cursor is in the project line, I’d still want it to move it to the first not @done.
Example (the cursor is the vertical line between the e and c):
Proje|ct B:
Task 1b @done
Task 2b @done
Task 3b
When would the cursor not be in any project? A note? A blank line?
You could experiment with affixing -or-self
to the ancestor
token in the path.
In the copy above, I’ve now edited the path template to:
`//@id=${cursorID}/ancestor-or-self::project//task not @done[0]`
When it’s in any untabbed (full left) notes or bullet items.
(Perhaps document notes before the first project, or between others, for example)
For slightly fuller messages (perhaps for a Keyboard Maestro notification) you could:
Perhaps, for a first draft:
//@id=${projectID}//@type=task and not @done[0]
(() => {
"use strict";
// Rob Trew @2021, 2024
// First not @done selected in project containing cursor
// Ver 2.03
// ------------------- JXA CONTEXT -------------------
// main :: IO ()
const main = () => {
const doc = Application("TaskPaper").documents.at(0);
return doc.exists()
? doc.evaluate({
script: `${TaskPaperContext}`
})
: "No documents open in TaskPaper";
};
// ---------------- TASKPAPER CONTEXT ----------------
// TaskPaperContext :: Editor -> IO ()
const TaskPaperContext = editor => {
const cursorToEndOfItem = item => (
editor.moveSelectionToItems(
item, item.bodyString.length
),
`Cursor at ${editor.selection.location}`
);
const
startItem = editor.selection.startItem,
project = [...startItem.ancestors, startItem].findLast(
x => "project" === x.getAttribute("data-type")
) || {},
projectID = project.id;
return undefined !== projectID
? (() => {
const
matches = editor.outline.evaluateItemPath(
`//@id=${projectID}//@type=task and not @done[0]`
);
return 0 < matches.length
? (
cursorToEndOfItem(matches[0]),
`${matches[0].bodyContentString}`
)
: `No remaining tasks in "${project.bodyContentString}".`
})()
: `Not in a project :: "${startItem.bodyContentString}".`;
};
// MAIN ---
return main();
})();
PS it should be possible, I think, to replace longer paths which search on @id
values with shorter paths applied to a contextItem, but I seem to be missing the trick of that, and getting the wrong match.
I like!
It returns an extra backslash and quote:
"Vitamins\""
Changing the scope, as you have a habit of further opening my eyes…
If there are no remaining undone tasks in the current project, or the cursor is not in a project, can the script fall back to positioning the cursor at the end of the first task that is not done (like in the original script?).
Amended above
Not this one
Fair.
Thank you yet again for your help!
I’ll take a look at the weekend