A Keyboard Maestro macro ( Mark Done and Sink
)
provisionally assigned to ⌘D
and with the options at the top of the JXA code set to:
withOptions: {
markSelectionsAsDone: true,
includeTime: true
}
Mark done and sink.kmmacros.zip (4.0 KB)
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// ROUGH EXPERIMENTAL DRAFT
// (not for use in production)
// @done in selected peer group triaged to below.
// Rob Trew @2021 MIT
// Ver 0.04
// Added option to mark selected items as @done
// before the triage.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ------------------- JXA CONTEXT -------------------
const main = () => {
const windows = Application("TaskPaper").windows;
return 0 < windows.length ? (
windows.at(0).document.evaluate({
script: `${TaskPaperContext}`,
withOptions: {
markSelectionsAsDone: true,
includeTime: true
}
})
) : "No windows open in TaskPaper.";
};
// ---------------- TASKPAPER CONTEXT ----------------
// eslint-disable-next-line max-lines-per-function
const TaskPaperContext = (editor, options) => {
const tpMain = () => {
const
outline = editor.outline,
selection = editor.selection,
selectionRoot = selection.startItem.parent;
let stampLog = "";
// -------- MUTATIONS GROUPED FOR ⌘Z ---------
outline.groupUndoAndChanges(() => {
stampLog = optionallyStamped(options)(
selection.selectedItems
);
// Updated TP3 outline,
foldTree(
updatedPeerOrder(outline)
)(
// from model of done-triaged peers.
foldTree(triagedByDone)(
pureTreeTP3(selectionRoot)
)
);
editor.moveSelectionToItems(
selectionRoot.firstChild
);
});
return stampLog;
};
// ---------------- DONE STAMPING ----------------
// optionallyStamped :: {
// markSelectionsAsDone :: Bool,
// includeTime :: Bool
// } -> [TP Item] -> IO String
const optionallyStamped = opts =>
items => opts.markSelectionsAsDone ? (() => {
const
nowStamp = timeIncluded(
opts.includeTime
)(
taskPaperDateString(new Date())
);
return items.flatMap(
x => Boolean(x.bodyString) ? (
x.setAttribute(
"data-done", nowStamp
),
[x.bodyString]
) : []
);
})() : "";
// ------------- PARTITIONED ON DONE -------------
// triagedByDone :: TPNode ->
// [TPNode] -> Tree TPNode
const triagedByDone = x =>
// Todos above, @done below.
compose(
Node(x),
concat,
partition(
t => !t.root.hasAttribute(
"data-done"
)
)
);
// -------- TP UPDATED FROM SORTABLE TREE --------
// updatedPeerOrder :: TPOutline ->
// TPNode -> [TPNode] -> IO Tree TPNode
const updatedPeerOrder = outline =>
// updatedPeerOrder(outline)
// can be used as an argument to foldTree
// for bottom up mapping of a new multi-level
// sorted Tree Node to the TaskPaper model.
tpNode => children => Node(tpNode)((
children.reduceRight(
(a, tree) => {
const x = tree.root;
return (
a.id !== x.id && (
outline
.insertItemsBefore(x, a)
),
x
);
},
tpNode.firstChild
),
tpNode.children
));
// ----------------- DATE STRING -----------------
// iso8601Local :: Date -> String
const iso8601Local = dte =>
new Date(dte - (6E4 * dte.getTimezoneOffset()))
.toISOString();
// taskPaperDateString :: Date -> String
const taskPaperDateString = dte => {
// A string representation of the given date:
// yyyy-mm-dd hh:mm (Abbreviated local ISO8601)
const [d, t] = iso8601Local(dte).split("T");
return [d, t.slice(0, 5)].join(" ");
};
// timeIncluded :: Bool -> String -> String
const timeIncluded = includeTime =>
// TaskPaper date string,
// with or without time component.
tpFullDate => includeTime ? (
tpFullDate
) : tpFullDate.slice(0, 10);
// -------------- GENERICS FOR TP3 ---------------
// Node :: a -> [Tree a] -> Tree a
const Node = v =>
// Constructor for a Tree node which connects a
// value of some kind to a list of zero or
// more child trees.
xs => ({
type: "Node",
root: v,
nest: xs || []
});
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
// A function defined by the right-to-left
// composition of all the functions in fs.
fs.reduce(
(f, g) => x => f(g(x)),
x => x
);
// concat :: [[a]] -> [a]
const concat = xs =>
xs.flat(1);
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => {
// The catamorphism on trees. A summary
// value obtained by a depth-first fold.
const go = tree => f(
tree.root
)(
tree.nest.map(go)
);
return go;
};
// partition :: (a -> Bool) -> [a] -> ([a], [a])
const partition = p =>
// A tuple of two lists - those elements in
// xs which match p, and those which do not.
xs => xs.reduce(
(a, x) => p(x) ? (
[a[0].concat(x), a[1]]
) : [a[0], a[1].concat(x)],
[
[],
[]
]
);
// pureTreeTP3 :: TP3Item -> Tree TP3Item
const pureTreeTP3 = item => {
// A tree in which the .root values are nodes
// in the TaskPaper native model, and the
// the .nest (children) lists are simple
// JS Arrays of trees – directly sortable etc.
const go = x =>
Node(x)(
x.hasChildren ? (
x.children.map(go)
) : []
);
return go(item);
};
// Taskpaper context main.
return tpMain();
};
// JXA main.
return main();
})();