I think it would be a good idea, and beneficiary to all the users of Taskpaper to create a welcome text file within the application to support to change or modify the “Welcome text” of TaskPaper to the user’s liking.
As I was going through some of the search posts, I figure it would be a good idea for me to include certain of those by default. I then modified the welcome text within the app to my liking, but thought that for others it was what some have already accused Taskpaper to be, “too geeky.”
If this is implemented, some people can share their default searches and doing so, educate the beginner user of the many ways power users are using the app. Like Doug said in one of his posts, [quote=“doug, post:7, topic:1628”]
but i found Matt Gemmell’s examples from his blogpost hugely useful
[/quote]
These are examples in a default text Matt Gemmell shared
My idea of changing the default “welcome text” was to change it to something that I can use all the time and change every blue moon.
When you proposed having a script or macro to do it, I thought… well, if I could have a folder with several of these “default texts” for certain jobs and I could pick and choose those when I am editing a document, that would be mind blowing awesome Something like those snippets that someone uses for coding, but for searches, tags, etc.
So perhaps a script which supplements the usual ⌘N with something like ⌘⌥ N for ‘New document based on template’, and simply throws up a list of templates in a folder somewhere ?
(Rather than fiddling with the resources in the .app package, which are refreshed with every update)
Although my original request was to have Taskpaper read a document in the application support folder and use that for new documents when the “Show Welcome Text” option was selected – if such document wasn’t available, then just read the welcomed text included in the .app package – I do like your idea better.
Here is a very rough first sketch – if you would like to test it and suggest adjustments, we could put a version into the script wiki, and perhaps make Keyboard Maestro and LaunchBar etc versions ?
(() => {
"use strict";
// 1. Create a folder containing two or more
// model TaskPaper documents
// 2. Edit the value of `fpTemplateFolder` at the top
// of this script to match the path the folder.
// 3. Run this script
// (e.g. from a Keyboard Maestro or Fastscripts shortcut)
ObjC.import("AppKit");
// TaskPaper 3 – new file from menu of templates.
// Ver 2.0
// Rob Trew @2021
const fpTemplateFolder = "~/TaskPaper Templates";
const templateUTI = "com.hogbaysoftware.taskpaper";
const title = "TaskPaper Templates";
// main :: IO ()
const main = () => {
const fpFolder = filePath(fpTemplateFolder);
return either(
alert("TaskPaper Templates")
)(
chosenTemplateName => chosenTemplateName
)(
bindLR(
doesDirectoryExist(fpFolder) ? (
Right(fpFolder)
) : Left(`Folder not found: ${fpFolder}`)
)(folderPath => {
const
templateNames = filesOfTypeInFolder(
templateUTI
)(folderPath)
.map(takeBaseName),
intFiles = templateNames.length;
return bindLR(
0 < intFiles ? (
1 < intFiles ? (
showMenuLR(false)(title)(
"Choose a template:"
)(templateNames[0])(
templateNames
)
) : Right([templateNames[0]])
) : Left(
`No ${templateUTI} templates` + (
` found in ${fpFolder}`
)
)
)(
chosenTemplateOpened(folderPath)
);
})
);
};
// chosenTemplateOpened :: FilePath -> [FileName]
// -> IO FilePath
const chosenTemplateOpened = fpFolder =>
choices => {
const
taskpaper = Application("TaskPaper"),
newDoc = taskpaper.Document(),
fpTemplate = combine(fpFolder)(
`${choices[0]}.taskpaper`
);
return (
// In TaskPaper,
taskpaper.activate(),
taskpaper.documents.push(newDoc),
newDoc.evaluate({
script: `${TaskPaperContext}`,
withOptions: {
textContent: readFile(
fpTemplate
)
}
}),
// in the JS interpreter.
Right(fpTemplate)
);
};
// filesOfTypeInFolder :: UTI -> FilePath -> [FileName]
const filesOfTypeInFolder = uti =>
fpFolder => listDirectory(fpFolder)
.flatMap(
fileName => either(
// Not a valid file path.
() => []
)(
fileUTI => uti !== fileUTI ? (
[]
) : [fileName]
)(
fileUTI(
combine(fpFolder)(
fileName
)
)
)
)
.sort();
// ---------------- TASKPAPER CONTEXT ----------------
const TaskPaperContext = (editor, options) =>
editor.outline.reloadSerialization(
options.textContent
);
// ----------------------- JXA -----------------------
// alert :: String => String -> IO String
const alert = alertTitle =>
s => {
const
sa = Object.assign(
Application("System Events"), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: alertTitle,
buttons: ["OK"],
defaultButton: "OK"
}),
s
);
};
// showMenuLR :: Bool -> String -> String ->
// [String] -> String -> Either String [String]
const showMenuLR = blnMult =>
// An optionally multi-choice menu, with
// a given title and prompt string.
// Listing the strings in xs, with
// the string `selected` pre-selected
// if found in xs.
menuTitle => prompt => selected => xs =>
0 < xs.length ? (() => {
const sa = Object.assign(
Application("System Events"), {
includeStandardAdditions: true
});
sa.activate();
const v = sa.chooseFromList(xs, {
withTitle: menuTitle,
withPrompt: prompt,
defaultItems: xs.includes(selected) ? (
[selected]
) : [xs[0]],
okButtonName: "OK",
cancelButtonName: "Cancel",
multipleSelectionsAllowed: blnMult,
emptySelectionAllowed: false
});
return Array.isArray(v) ? (
Right(v)
) : Left(`User cancelled ${title} menu.`);
})() : Left(`${title}: No items to choose from.`);
// --------------------- 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);
// doesDirectoryExist :: FilePath -> IO Bool
const doesDirectoryExist = fp => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(fp)
.stringByStandardizingPath, ref
) && ref[0];
};
// 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);
// fileUTI :: FilePath -> Either String String
const fileUTI = fp => {
// ObjC.import('AppKit')
const
e = $(),
uti = $.NSWorkspace.sharedWorkspace
.typeOfFileError(fp, e);
return uti.isNil() ? (
Left(ObjC.unwrap(e.localizedDescription))
) : Right(ObjC.unwrap(uti));
};
// listDirectory :: FilePath -> [FilePath]
const listDirectory = fp =>
ObjC.unwrap(
$.NSFileManager.defaultManager
.contentsOfDirectoryAtPathError(
ObjC.wrap(fp)
.stringByStandardizingPath,
null
))
.map(ObjC.unwrap);
// readFile :: FilePath -> IO String
const readFile = fp => {
// The contents of a text file at the
// filepath fp.
const
e = $(),
ns = $.NSString
.stringWithContentsOfFileEncodingError(
$(fp).stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
);
return ObjC.unwrap(
ns.isNil() ? (
e.localizedDescription
) : ns
);
};
// 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;
})() : ""
) : "";
return main();
})();
I like the script approach for now too. Maybe someday I would make a template document in the application support folder, but I think I’d rather live with the current “copy paste” favorite searches approach for a while. I think in the end it might make more sense to add some “built in” searches (that can be edited in preferences or something) so that you can use saved searches without embedding them in your doc. Then you could just used in document saved searches for very specific cases.
Hey guys, the KM script above works great however it creates a new file instead of appending the template to an existing file (it’s listed on the TP wiki as “A script to append templates in a folder to document”).
Has anyone tried to modify it to make it append to an existing file instead of creating a new file?
Another idea would be to have it add the contents of the template to the open document (perhaps to wherever the text cursor is).
My goal would be to have template for projects / searches that I could add to larger taskpaper files instead of having templates for entire files.
I guess that’d be quite useful because I could just add another action to the KM macro to make it immediately paste the clipboard to wherever my cursor is at.
For a similar macro which copies the contents of the template to clipboard (rather than creating a new document with it), you can use a lightly modified version of the code in the Keyboard Maestro Execute JavaScript for Automation action:
While this is not a huge problem, I’d just like to point out that the script doesn’t deal that well with special characters such as ç, ã, ó, etc.
For example, if a template contains the word “documentação” (portuguese for documentation), the script will copy it as “documenta√ß√£o”.
This is not a deal breaker however I believe other users might welcome a fix for this issue as well.