Perhaps something like this JavaScript for Automation script ?
(which could be launched, for example, from Keyboard Maestro in an Execute a JavaScript for Automation action. See also Taskpaper - Using Scripts)
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// Rob Trew @2021
// Ver 0.01
// main :: IO ()
const main = () => {
// Edit to match the root path for your projects:
const projectsRootPath = "~/projects";
const
taskpaper = Application("TaskPaper"),
windows = taskpaper.windows;
return either(
alert("Folder for selected task")
)(
fp => Object.assign(
Application.currentApplication(), {
includeStandardAdditions: true
}
)
.displayNotification(fp, {
withTitle: "Project folder:",
soundName: "glass"
})
)(
bindLR(
0 < windows.length ? (
Right(windows.at(0).document)
) : Left("No documents open in TaskPaper")
)(
document => bindLR(
document.evaluate({
script: `${TP3Context}`
})
)(
folderPathFoundOrCreatedLR(
projectsRootPath
)
)
)
);
};
// folderPathFoundOrCreatedLR :: FilePath ->
// [String] -> IO FilePath
const folderPathFoundOrCreatedLR = rootPath =>
projects => 0 < projects.length ? (
bindLR(
createDirectoryIfMissingLR(true)(
combine(rootPath)(
projects.join("/")
)
)
)(
openedInFinderLR
)
) : Left(
"TaskPaper selection not in a project."
);
// openedInFinderLR :: FilePath -> IO FilePath
const openedInFinderLR = fpFolder => {
const finder = Application("Finder");
return (
finder.open(Path(fpFolder)),
finder.activate(),
Right(fpFolder)
);
};
// ---------------- TASKPAPER CONTEXT ----------------
// TP3Context :: Editor -> IO Either String [String]
const TP3Context = editor => {
const item = editor.selection.startItem;
return Boolean(item) ? {
Right: item.ancestors.concat(item).flatMap(
x => "project" === x.getAttribute(
"data-type"
) ? [
x.bodyContentString
] : []
)
} : {
Left: "Nothing selected in TaskPaper"
};
};
// ----------------------- JXA -----------------------
// alert :: String => String -> IO String
const alert = title =>
s => {
const sa = Object.assign(
Application("System Events"), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ["OK"],
defaultButton: "OK"
}),
s
);
};
// createDirectoryIfMissingLR :: Bool ->
// FilePath -> Either String FilePath
const createDirectoryIfMissingLR = blnParents =>
dirPath => {
const fp = filePath(dirPath);
return doesPathExist(fp) ? (
Right(fp)
) : (() => {
const
e = $(),
blnOK = $.NSFileManager
.defaultManager[
"createDirectoryAtPath" + (
"WithIntermediateDirectories"
) + "AttributesError"
](fp, blnParents, void 0, e);
return blnOK ? (
Right(fp)
) : Left(e.localizedDescription);
})();
};
// doesPathExist :: FilePath -> IO Bool
const doesPathExist = fp =>
$.NSFileManager.defaultManager
.fileExistsAtPath(
$(fp).stringByStandardizingPath
);
// filePath :: String -> FilePath
const filePath = s =>
// The given file path with any tilde expanded
// to the full user directory path.
ObjC.unwrap(ObjC.wrap(s)
.stringByStandardizingPath);
// --------------------- GENERIC ---------------------
// Left :: a -> Either a b
const Left = x => ({
type: "Either",
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: "Either",
Right: x
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => m.Left ? (
m
) : mf(m.Right);
// combine (</>) :: FilePath -> FilePath -> FilePath
const combine = fp =>
// Two paths combined with a path separator.
// Just the second path if that starts with
// a path separator.
fp1 => Boolean(fp) && Boolean(fp1) ? (
"/" === fp1.slice(0, 1) ? (
fp1
) : "/" === fp.slice(-1) ? (
fp + fp1
) : `${fp}/${fp1}`
) : fp + fp1;
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
// Application of the function fl to the
// contents of any Left value in e, or
// the application of fr to its Right value.
fr => e => e.Left ? (
fl(e.Left)
) : fr(e.Right);
// MAIN ---
return main();
})();
Zipped sample macro for Keyboard Maestro 10:
Project-path folder opened.kmmacros.zip (3.1 KB)