I was wondering/hoping that you would add a checkbox feature to Bike. One way I use outlines is to create a checklist for my students for their online courses. Having an actual checkbox option (along with maybe a legal numbering theme/option) would make Bike my sole outlining program.
Bike is working in that direction though maybe a while and not sure exactly where it will end up. I expect the next related feature will be inline formatting and ability to format “strikethrough” text.
If and when you do end up implementing a concept of “checked” items, a small feature idea here that I never realised I needed until I used outliners that offered it: “Pruning” checked items in either the whole document, or a specific row (and its children).
I use outlines extensively for managing projects and like to hold on to checked items for a while, but on a regular basis delete all checked items nested under a specific row to spring clean.
Until we get actual checkboxes, I am making do with a text expansion shortcut that inserts the ︎ unicode symbol.
Thanks for the tip. That would work for me, but I doubt most of my students would find it easy enough to do. Plus, I generally convert the outline to a PDF that they can actually print and check-off each box (I know, very old-school analog).
Should be possible, I think, to write a script to toggle ☐☑ characters at the start of selected lines.
(I’ll take a look at the weekend)
In the meanwhile, if you just need a ☐ prefix for printing, you could toggle it on and off in selected lines by using an existing prefix-toggling script:
Here as a Keyboard Maestro macro:
BIKE - Toggle a box character prefix in selected lines.kmmacros.zip (2.4 KB)
and here as a JS script to attach to a keystroke in some other way:
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// Toggle a given prefix character in selected lines
// of Bike 1.2
// Rob Trew @2022
// Ver 0.01
// --------------------- OPTION ----------------------
// Which single-character prefix to add or clear ?
const
prefixChar = "☐";
// Application("Keyboard Maestro Engine")
// .getvariable("prefixChar");
// ------------ PREFIX CHARACTER TOGGLED -------------
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists() ? (() => {
const
selectedRows = doc.rows.where({
selected: true
}),
n = selectedRows.length;
return Boolean(n) ? (() => {
const [f, change] = (
selectedRows.at(0).name()
.startsWith(prefixChar)
) ? (
[dePrefixed(prefixChar), "CLEARED"]
) : [prefixed(prefixChar), "ADDED"];
return (
zipWith(row => s => row.name = s)(
selectedRows()
)(
selectedRows.name().map(f)
),
[
`${change} '${prefixChar}' prefix`,
`in ${n} selected lines.`
]
.join("\n")
);
})() : "No rows selected in Bike";
})() : "No documents open in Bike";
};
// -------------------- PREFIXES ---------------------
// prefixed :: Char -> String -> String
const prefixed = c =>
s => `${c} ${dePrefixed(c)(s)}`;
// dePrefixed :: Char -> String -> String
const dePrefixed = c =>
s => c === s[0] ? (
s.slice(" " === s[1] ? 2 : 1)
) : s;
// --------------------- GENERIC ---------------------
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = f =>
// A list constructed by zipping with a
// custom function, rather than with the
// default tuple constructor.
xs => ys => xs.map(
(x, i) => f(x)(ys[i])
).slice(
0, Math.min(xs.length, ys.length)
);
// MAIN
return main();
})();
See: Using Scripts - Bike
It might make the most sense to support Markdown checkboxes, like:
[ ] unfinished task
[x] finished task
OPML doesn’t support “checklists,” though OmniOutliner, for instance, uses the attribute _status="checked"
for checked items.
A Markdown checkbox toggling script:
To test in Script Editor.app (with language selector at top left set to JavaScript)
See: Using Scripts - Bike
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// Cycle a set of prefixes (single or multi character)
// in selected (non-empty) lines of Bike 1.3
// Rob Trew @2022
// Ver 0.01
// --------------------- OPTION ----------------------
// Which prefixes to cycle ?
// (An empty string is interpreted as a
// no-prefix stage in the cycle).
const prefixCycle = ["[ ]", "[x]", ""];
// ----------------- PREFIX TOGGLED ------------------
// main :: IO ()
const main = () => {
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists() ? (() => {
const
selectedNonEmptyRows = doc.rows.where({
selected: true,
_not: [{
name: ""
}]
});
return Boolean(selectedNonEmptyRows.length) ? (
bikeRowsPrefixCycled([...prefixCycle])(
selectedNonEmptyRows
)
) : "No non-empty rows selected in Bike.";
})() : "No documents open in Bike.";
};
// ------------------ PREFIX CYCLED ------------------
// bikeRowsPrefixCycled :: [String] ->
// IO Bike Rows -> IO String
const bikeRowsPrefixCycled = prefixes =>
// Any selected non-empty rows in Bike cycled
// to the the next prefix in prefixes
selectedNonEmptyRows => {
const
n = selectedNonEmptyRows.length,
plural = 1 < n ? "s" : "",
[f, change] = (() => {
const
iPrefix = prefixes
.findIndex(
k => selectedNonEmptyRows
.at(0).name()
.startsWith(k)
),
nextPrefix = prefixes[
-1 !== iPrefix ? (
(1 + iPrefix) % prefixes
.length
) : 0
];
return [
updatedLine(prefixes)(nextPrefix),
"" !== nextPrefix ? (
`Set ${nextPrefix}`
) : "Cleared"
];
})();
return (
zipWith(row => s => row.name = s)(
selectedNonEmptyRows()
)(
selectedNonEmptyRows.name().map(f)
),
[
`${change} prefix`,
`in ${n} selected line${plural}.`
]
.join("\n")
);
};
// updatedLine :: [String] -> String -> String -> String
const updatedLine = prefixes =>
// A line in which the prefix has been
// replaced by the next option in a cycle.
// A space character in the prefixes list is
// interpreted as no-prefix stage in the cycle.
pfx => s => {
const pre = Boolean(pfx) ? `${pfx} ` : "";
return Boolean(s) ? (() => {
const
iExisting = prefixes.findIndex(
k => Boolean(k) && s.startsWith(k)
);
return -1 !== iExisting ? (
pre + s.slice(
1 + prefixes[iExisting].length
)
) : (pre + s.trim());
})() : pre;
};
// --------------------- GENERIC ---------------------
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = f =>
// A list constructed by zipping with a
// custom function, rather than with the
// default tuple constructor.
xs => ys => xs.map(
(x, i) => f(x)(ys[i])
).slice(
0, Math.min(xs.length, ys.length)
);
// MAIN
return main();
})();
Or to use with something like FastScripts or Keyboard Maestro:
BIKE – Toggle a Markdown checkbox in selected lines.kmmacros.zip (2.8 KB)
Nice, thanks for the continuing collection of Keyboard Maestro scripts! Really helpful.