You could test this in Script Editor (language selector at top left set to JavaScript
)
Menu of Taskpaper files in given directory.
Contents of chosen file copied to clipboard:
Expand disclosure triangle to view JS Source
(() => {
"use strict";
const fpTemplatesFolder = "~/Taskpaper templates";
// Menu of Taskpaper files in given directory.
// Contents of chosen file copied to clipboard.
// Rob Trew @2021
// Ver 0.01
ObjC.import("AppKit");
// main :: IO ()
const main = () =>
either(
msg => msg.startsWith("User cancelled") ? ("") : (
alert("Copy Taskpaper template to clipboard"),
msg
)
)(
x => x
)(
bindLR(
getDirectoryContentsLR(fpTemplatesFolder)
)(
xs => bindLR(
compose(
templateChoiceLR(fpTemplatesFolder),
sortOn(toLower),
filter(x => x.endsWith(".taskpaper"))
)(xs)
)(
templateName => bindLR(
readFileLR(
combine(fpTemplatesFolder)(
templateName
)
)
)(
txt => 0 < txt.trim().length ? (
copyText(txt),
Right(`Copied contents of: ${templateName}`)
) : Left(`Empty: ${templateName}`)
)
)
)
);
// templateChoice :: filePath ->
// [FilePath] -> Either String String
const templateChoiceLR = folderPath =>
fileNames => 0 < fileNames.length ? (() => {
const
sa = Object.assign(
Application.currentApplication(), {
includeStandardAdditions: true
}),
legend = `\n\n\t${folderPath}\n\n`,
menu = fileNames.map(takeBaseName),
choice = sa.chooseFromList(menu, {
withTitle: "Copy template contents to clipboard",
withPrompt: `Templates in folder:${legend}Choose:`,
defaultItems: menu[0],
okButtonName: "Copy file contents",
cancelButtonName: "Cancel",
multipleSelectionsAllowed: false,
emptySelectionAllowed: true
});
return Array.isArray(choice) ? (
0 < choice.length ? (
Right(`${choice[0]}.taskpaper`)
) : Left("No template chosen.")
) : Left("User cancelled.");
})() : Left(`No templates found in: ${folderPath}`);
// ----------------------- 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
);
};
// copyText :: String -> IO String
const copyText = s => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// getDirectoryContentsLR :: FilePath ->
// Either String IO [FilePath]
const getDirectoryContentsLR = fp => {
const
error = $(),
xs = $.NSFileManager.defaultManager
.contentsOfDirectoryAtPathError(
$(fp).stringByStandardizingPath,
error
);
return xs.isNil() ? (
Left(ObjC.unwrap(error.localizedDescription))
) : Right(ObjC.deepUnwrap(xs));
};
// readFileLR :: FilePath -> Either String IO String
const readFileLR = fp => {
// Either a message or the contents of any
// text file at the given filepath.
const
e = $(),
ns = $.NSString
.stringWithContentsOfFileEncodingError(
$(fp).stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
);
return ns.isNil() ? (
Left(ObjC.unwrap(e.localizedDescription))
) : Right(ObjC.unwrap(ns));
};
// --------------------- 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
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = a =>
b => ({
type: "Tuple",
"0": a,
"1": b,
length: 2,
*[Symbol.iterator]() {
for (const k in this) {
if (!isNaN(k)) {
yield this[k];
}
}
}
});
// 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;
// comparing :: (a -> b) -> (a -> a -> Ordering)
const comparing = f =>
x => y => {
const
a = f(x),
b = f(y);
return a < b ? -1 : (a > b ? 1 : 0);
};
// 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
);
// 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);
// filter :: (a -> Bool) -> [a] -> [a]
const filter = p =>
// The elements of xs which match
// the predicate p.
xs => [...xs].filter(p);
// fst :: (a, b) -> a
const fst = tpl =>
// First member of a pair.
tpl[0];
// snd :: (a, b) -> b
const snd = tpl =>
// Second member of a pair.
tpl[1];
// sortOn :: Ord b => (a -> b) -> [a] -> [a]
const sortOn = f =>
// Equivalent to sortBy(comparing(f)), but with f(x)
// evaluated only once for each x in xs.
// ("Schwartzian" decorate-sort-undecorate).
xs => xs.map(
x => Tuple(f(x))(x)
)
.sort(uncurry(comparing(fst)))
.map(snd);
// takeBaseName :: FilePath -> String
const takeBaseName = fp =>
("" !== fp) ? (
("/" !== fp[fp.length - 1]) ? (() => {
const fn = fp.split("/").slice(-1)[0];
return fn.includes(".") ? (
fn.split(".").slice(0, -1)
.join(".")
) : fn;
})() : ""
) : "";
// toLower :: String -> String
const toLower = s =>
// Lower-case version of string.
s.toLocaleLowerCase();
// uncurry :: (a -> b -> c) -> ((a, b) -> c)
const uncurry = f =>
// A function over a pair, derived
// from a curried function.
(...args) => {
const [x, y] = Boolean(args.length % 2) ? (
args[0]
) : args;
return f(x)(y);
};
return main();
})();