What I’m looking for is the ability to work with Bike Outliner similar to how one would use a program like e.g. `grep` in shell scripting, i.e. it has an input and an output, and it’s purpose is to apply a transformation to the input. In this case, the transformation would be the user modifying the outline in the GUI until they’re satisfied with it.
Whether that’s realized via direct input/output or some sort of temporary file or scratch file doesn’t matter too much to me I guess. But since I’d like to use it as a step in an automation pipeline, I’d need the GUI to somehow reflect the use case; i.e. (broadly speaking) maybe not display a file name at all, but float the window, and feature cancel/done buttons or something along those lines to let the user continue with the automation once they’re done.
Imagine it basically exactly like how the Quick capture window of Drafts.app works. So, thinking about it, this could probably, at least on the UI side, be generalized into a genuine quick capture window. Then it would only have to be able to work with both a new capture as well as have the ability to take some input from somewhere as part of an automation and pass the output along later.
My use case for this rather specific request is that it’s really helpful for me to be able to take some sort of list (e.g. from markdown or just plain text) and be able to structure it on the fly by hand before then working with it in some way; and all that without the friction of a new document, which then needs to be abandoned later. And an already quite advanced outliner like Bike would be a much better tool for that than trying to put something together myself.
Besides that, a general quick capture functionality would make Bike an even more incredibly powerful tool for me. The ability to play around with thoughts and give them proper structure just a keybind away would be amazing; no friction, just pure freedom to let the thoughts flow.
Let me know what you think! Maybe some elements of this could be implemented with extensions, but I’m not up to speed on their capabilities tbh, although at the very least the quick capture UI would of course have to be implemented in Bike Outliner itself.
Sounds like a fun idea. I have played around with extensions a little, and I think you could get pretty close with an extension + maybe some other shell or programming logic.
How comfortable are you with programming your own extension?
I think, roughly speaking, there are different components to this quick capture pipeline. The first part where you generate a potentially new or temp file in a specified directory may be possible with extensions, but if not you could use a simple script to touch a new file, and the open the outline via a command like bike open outline myFile.md.
The second part where you type in this temp file is obviously within Bike, although you may want this window to have certain proportions. E.g. maybe for quick capture you want it to be a smaller window, centered on screen. I don’t think extensions can manipulate window positioning and size (though maybe I’m wrong), but here you could use something like Hammerspoon or a custom Swift script to modify the window via the Mac accessibility API.
Finally, you have to specify that you’re done, which perhaps you will create a Bike extension command for, and then you might need another script which will take this finished quick capture file and do whatever else you had in mind in the pipeline.
I like it. And good test for Bike’s automation systems!
Have you looked at Bike 2’s new command line interface? I think it might provide all the pieces you need.
It can:
Create temp outline with nice name such as “Pipe”
Insert input rows in appropriate format
Open in Bike where you can edit
While script watches Bike, waiting for you to close “Pipe”
At which point it reads the file and passes output on
I’m just leaving off to supper. Here’s what I’ve got for you… AI generated. Untested. With that said I’m pretty sure the idea can be made to work, just not sure if this script does it.
Update
I’ve updated the script to use Bike’s observe command instead of polling. And I tested it (once), seems to work!
What I did:
cat notes.md | ./bike-pipe.sh | pbcopy
And the result is an outline is opened with the contents of notes.md. Edit as you see fit. And when you close document your edits are put on the pasteboard.
#!/usr/bin/env bash
#
# bike-pipe — use Bike Outliner as a filter in a shell pipeline.
#
# Reads content on stdin, opens it in a floating "Pipe.bike" capture window for
# the user to restructure by hand, then — once they close the window — writes
# the finished outline to stdout. Think of it as `$EDITOR` for outlines:
#
# cat notes.md | ./bike-pipe.sh | pbcopy
# ./bike-pipe.sh < tasks.txt > structured.md # plain-text list -> outline
# echo | ./bike-pipe.sh # empty stdin = blank quick-capture
#
# Pipeline contract:
# • stdin — initial content (Bike-flavored markdown; plain text & "- " lists
# also work, tab indentation becomes nesting). Empty stdin opens a
# blank capture window.
# • stdout — the final outline after the user closes the window.
# • stderr — status/instructions (so stdout stays clean for piping).
#
# The output format is markdown by default; override with --format or
# BIKE_PIPE_FORMAT (markdown | txt | bike | opml | json).
#
# Requires: Bike.app running (with a license) and the `bike` CLI on PATH, plus `jq`.
set -uo pipefail
# --- config ----------------------------------------------------------------
FORMAT="${BIKE_PIPE_FORMAT:-markdown}"
KEEP_TEMP="${BIKE_PIPE_KEEP:-0}" # 1 = leave Pipe.bike on disk
while [ $# -gt 0 ]; do
case "$1" in
-f|--format) FORMAT="${2:?--format needs a value}"; shift 2 ;;
--keep) KEEP_TEMP=1; shift ;;
-h|--help)
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;;
*) echo "bike-pipe: unknown argument: $1" >&2; exit 2 ;;
esac
done
# --- preflight -------------------------------------------------------------
command -v bike >/dev/null 2>&1 || { echo "bike-pipe: 'bike' CLI not found on PATH." >&2; exit 127; }
command -v jq >/dev/null 2>&1 || { echo "bike-pipe: 'jq' is required." >&2; exit 127; }
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/bike-pipe.XXXXXX")"
PIPE="$WORK_DIR/Pipe.bike" # step 1: the scratch file lives in temp
IN_FILE="$WORK_DIR/input.md"
STREAM="$WORK_DIR/stream.out"
cleanup() {
[ "$KEEP_TEMP" = "1" ] || rm -rf "$WORK_DIR"
}
trap cleanup EXIT
# Capture whatever arrived on stdin (may be empty for a fresh quick-capture).
cat > "$IN_FILE"
# --- step 1+3: create the scratch file and open it in the GUI --------------
# `create outline <path>` writes a new empty .bike file AND opens it in the GUI.
OPEN_JSON="$(bike create outline "$PIPE" --format bike 2>&1)" || {
echo "bike-pipe: failed to create/open $PIPE" >&2
echo "$OPEN_JSON" >&2
exit 1
}
# --- step 2: replace the file's contents with the incoming content ---------
# A brand-new Bike doc opens empty (zero rows), so importing is already a clean
# "replace". As defensive insurance against a future version that seeds a
# placeholder row, record the ids present *before* we import so we can strip any
# leftover empty row after — leaving exactly the piped-in content.
SEED_IDS="$(bike get outline rows '//*' --outline "$PIPE" -o session 2>/dev/null \
| jq -r '.. | objects | .id? // empty' 2>/dev/null)"
if [ -s "$IN_FILE" ]; then
if ! bike create rows -f "$IN_FILE" --outline "$PIPE" >/dev/null 2>&1; then
echo "bike-pipe: warning: failed to import stdin content" >&2
fi
# Delete each seed row, but only if it is still empty (so we can never
# remove a line the user actually piped in).
for id in $SEED_IDS; do
txt="$(bike get outline rows "$id" --outline "$PIPE" -o txt 2>/dev/null | tr -d '[:space:]')"
[ -z "$txt" ] && bike delete rows "$id" --outline "$PIPE" >/dev/null 2>&1 || true
done
fi
# --- step 4: wait for the user, then emit the result -----------------------
echo "bike-pipe: editing in Bike — close the Pipe.bike window when you're done." >&2
# Event-driven wait. A *pinned* observe (explicit --outline) streams content
# snapshots and is torn down — ending the stream, so this command returns — the
# instant the outline is closed. No polling. `--rows` drops Bike's document
# wrapper so the output is clean content; `--debounce 0` emits on every edit so
# the last snapshot reflects the user's final keystroke (a programmatic close —
# and a "Don't Save" close — never flushes those edits to disk).
bike observe outline --outline "$PIPE" -o "$FORMAT" --rows --debounce 0 > "$STREAM" 2>/dev/null || true
# The window is gone. Prefer the last live snapshot we streamed: it reflects the
# in-memory state the user last saw, which the on-disk file can lag behind (or
# miss entirely) when the close didn't save. observe delimits successive text
# snapshots with a "\n\n---\n\n" record separator, so the last non-empty record
# is that final state. Fall back to a headless read of the file only if the
# stream gave us nothing (e.g. observe never started).
FINAL=""
if [ -s "$STREAM" ]; then
FINAL="$(awk 'BEGIN{RS="\n\n---\n\n"} NF{last=$0} END{printf "%s", last}' "$STREAM")"
fi
if [ -z "${FINAL//[$'\n\t ']/}" ]; then
FINAL="$(bike get outline rows '//*' --outline "$PIPE" -o "$FORMAT" 2>/dev/null)"
fi
printf '%s\n' "$FINAL"
echo "bike-pipe: done." >&2