New Feature Request: Incrementing Variables a Tool for Writers

Just my .02 on a new feature request. I’d love to see some kind of insertable variable. Example - if I was outlining a book, I want the ability to move chapters around and right now I don’t see a way to insert a variable.

My first use for bike is organizing a set of tweet storms which look like this:

Startup Lesson #001: Real World Venture Capital - You Can Only Raise Money 7 or 8 Months of the Year

and then there’s:

Startup Lesson #002: Every problem you have will always repeat unless you actively solve it.

But #001 was originally 012 until I realized I needed to get people with something controversial.

Anyway – lovely product; makes me think of More or ThinkTank from back in the day. And Doug Englebart himself would be proud (I met him once). Great job!!!

1 Like

Oh and I was stupid; my thought is at the same indent level the variable just increments up by 1. Yes there’s a lot more you could do but that’s a start.

Glad you are liking Bike.

Maybe variables will get added someday in distant future, but I don’t expect that it will happen soon. First, I’m still working on lots of very basic things that I think should be done first. Second I’m not sure that I fully understand the feature, it would be very helpful if you could add some links to other programs that support variables in the way that you would want.

In the meantime (if this reordering, renumbering) becomes a frequent problem I think your best bet would be to write a script to perform the numbering for you.

We would probably need a before and after example to form a clear picture of what you are envisaging there.

With a bit of clarity and some specifics, it’s possible we might be able to sketch out a script for you.

1 Like

I think nothing more, at this point, than @counter or @ctr and then a script which increments them would be enough.

Honestly I think you really need an understanding of how people are using it. If people are using Bike for organization then this is a non-request. If they are using it for structured writing (books with chapters) then it really matters. The power of an outliner is movement. But movement when you then have to manually adjust EVERY damn node title – that kinda sucks.

This used to be a pretty standard thing back in the days of Grandview (I think that’s the right name; don’t have my outliner history handy).

1 Like

The value of these tags is ordinal ?

If we start with:

- alpha #1
- beta #2
- gamma #3

(with successive values incremented for us by a script)

and then drag alpha down to the end, should the order numbers be respected and preserved:

- beta #2
- gamma #3
- alpha #1

or do you need the next script run to replace the existing numbers with new numbers which match the new order ?

- beta #1
- gamma #2
- alpha #3

?

i.e. the tag values show us:

  • an ordering in time – the order in which things were written ?
  • a positional ordering – the order in which they are now shown, from top of document to bottom ?

In the meanwhile, for initial experimentation on scratch texts, rather than use on real documents, a couple of Keyboard Maestro macros:

togglingAndUpdatingTOCtags.kmmacros.zip (4.9 KB)

  • Toggle a @toc tag on or off in selected Bike rows (initially ⌘T)
  • Update all @toc tag values to contain outline numbering based on their position. (initially ⌘U)

Sample of a document containing updated @toc tags:

Expand disclosure triangle to view a sample text
Welcome to Bike!

Use Bike to record and process your ideas. You'll need to spend a little time learning Bike to get the most out of it.

Bike Home Page @toc(1)
	Watch intro movie
	Glance through features list
	https://www.hogbaysoftware.com/bike
Bike User's Guide @toc(2)
	Read Getting Started section
	Glance through other sections for future reference
	https://bikeguide.hogbaysoftware.com
Keyboard Shortcuts @toc(3)
	Row @toc(3.1)
		Create Row: Return @toc(3.1.1)
		Delete Row: Escape to outline mode, then Delete @toc(3.1.2)
		Indent Row: Tab or Control-Command-Right @toc(3.1.3)
		Unindent Row: Shift-Tab or or Control-Command-Left @toc(3.1.4)
		Move Row Up: Control-Command-Up @toc(3.1.5)
		Move Row Down: Control-Command-Down @toc(3.1.6)
	Outline @toc(3.2)
		Focus In: Option-Command-Right @toc(3.2.1)
		Focus Out: Option-Command-Left
		Expand Row: Command-0 or Escape to outline mode, then Right
		Collapse Row: Command-9 or Escape to outline mode, then Left @toc(3.2.2)
		Expand All Rows: Control-Command-0
		Collapse All Rows: Control-Command-9
	More @toc(3.3)
		Toggle text/outline mode: Escape
		Close Find Panel: Escape
		Close Check Panel Escape

It is also possible to use these scripts without Keyboard Maestro, for example by using something like FastScripts to bind them to keystrokes.

If you test them in Script Editor.app, you would to set the language selector at top left to JavaScript rather than AppleScript.

See: Using Scripts in Bike

Updating the outline-numbering values of any @toc tags in the document, for example after:

  1. moving paragraphs, or
  2. adding more @toc tags.
Expand disclosure triangle to view JS Source – Updating @toc values
(() => {
    "use strict";

    // Updating the value of any @ord tags found
    // with outline numbering strings based on their
    // position in the outline.

    // Rob Trew @2022
    // Ver 0.01

    const tagPrefix = "@toc";

    // main :: IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);
            
        return doc.exists() ? (() => {
            const 
                taggedRows = doc.rows.where({
                    name: {
                        _contains: tagPrefix
                    }
                });

            return rowsUpdated(tagPrefix)(doc)(
                taggedNodesInForest(tagPrefix)(
                    partiallyOutlineNumberedForest(
                        Math.min(...taggedRows.level())
                    )(tagPrefix)([
                        Node({
                            text: "virtualRoot"
                        })(
                            bikeDocForestWithIDs(doc)
                        )
                    ])
                )    
            );
        })() : "No document open in Bike.app";
    };


    // rowsUpdated :: String -> Bike Document ->
    // [Dict] -> IO String
    const rowsUpdated = tagPrefix =>
        // Updates of the row.name() properties in rows
        // identified by the `id` value in each node.
        // In addition to the update effects, the function
        // returns a listing of the updated texts.
        doc => nodes => {
            const rgxTag = new RegExp(
                `(${tagPrefix}\\(.*\\)|${tagPrefix})`, 
                "gu"
            );

            return nodes.map(dict => {
                const
                    newText = updatedText(rgxTag)(
                        tagPrefix
                    )(dict.text)(dict.ords),
                    row = doc.rows.byId(dict.id);

                return (
                    row.name = newText,
                    newText
                );
            })
            .join("\n");
        };


    // taggedNodesInForest :: String -> 
    // Forest Dict -> [Dict]
    const taggedNodesInForest = tag =>
        // A list of those nodes in the given forest
        // which contain the specified tag.
        forest => forest.flatMap(
            foldTree(
                x => xs => x.text.includes(tag) ? (
                    [x].concat(xs.flat())
                ) : xs.flat()
            )
        );


    // updatedTag :: Regex -> String -> String -> 
    // [Int] -> String
    const updatedText = tagRegex =>
        // Node text in which the value of the given tag has
        // been updated with a dot-delimited outline label.
        tag => txt => ns => txt.replace(
            tagRegex,
            `${tag}(${ns.join(".")})`
        );


    // partiallyOutlineNumberedForest :: Int -> String ->
    // Forest {text::String} ->  
    // Forest {text::String, ords::[Int]}
    const partiallyOutlineNumberedForest = topLevel =>
        // A forest in which each node is decorated 
        // from a certain level downwards, with
        // an outline-numbering list of integers.
        tagPrefix => forest => {
            const lastIncremented = xs =>
                Boolean(xs.length) ? [
                    ...xs.slice(0, -1), 
                    1 + xs.slice(-1)[0]
                ] : [];

            const go = level => ns => tree => {
                const
                    dict = root(tree),
                    txt = dict.text,
                    labellingStarted = level >= topLevel;

                return Tuple(
                    txt.includes(tagPrefix) ? (
                        lastIncremented(ns)
                    ) : ns
                )(
                    Node(
                        Object.assign({
                                ords: labellingStarted ? (
                                    ns
                                ) : []
                            },
                            dict
                        )
                    )(
                        mapAccumL(go(1 + level))(
                            labellingStarted ? (
                                [...ns, 1]
                            ) : ns
                        )(nest(tree))[1]
                    )
                );
            };

            return mapAccumL(
                go(1)
            )([])(forest)[1];
        };


    // ---------------------- BIKE -----------------------

    // bikeDocForestWithIDs :: Bike Doc -> [Tree Dict]
    const bikeDocForestWithIDs = doc => {
        // A forest of strings representing the outline(s)
        // in a Bike 1.3 document.
        const rows = doc.rows;

        return forestWithIDsFromIndentedLines(
            zip(
                rows.level()
            )(
                zip(
                    rows.id()
                )(
                    rows.name()
                )
            )
        );
    };


    // forestWithIDsFromIndentedLines :: [(Int, String)] ->
    // [Tree {id: String, text:String, body:Int}]
    const forestWithIDsFromIndentedLines = tuples => {
        const go = xs =>
            0 < xs.length ? (() => {
                // First line and its sub-tree,
                const [level, [id, text]] = xs[0],
                    [tree, rest] = span(x => level < x[0])(
                        xs.slice(1)
                    );

                return [
                        Node({
                            id,
                            text,
                            level
                        })(go(tree))
                    ]
                    // followed by the rest.
                    .concat(go(rest));
            })() : [];

        return go(tuples);
    };


    // --------------------- GENERIC ---------------------

    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
        // Constructor for a Tree node which connects a
        // value of some kind to a list of zero or
        // more child trees.
        xs => ({
            type: "Node",
            root: v,
            nest: xs || []
        });


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f => {
        // The catamorphism on trees. A summary
        // value obtained by a depth-first fold.
        const go = tree => f(
            root(tree)
        )(
            nest(tree).map(go)
        );

        return go;
    };


    // mapAccumL :: (acc -> x -> (acc, y)) -> acc ->
    // [x] -> (acc, [y])
    const mapAccumL = f =>
        // A tuple of an accumulation and a list
        // obtained by a combined map and fold,
        // with accumulation from left to right.
        acc => xs => [...xs].reduce(
            ([a, bs], x) => second(
                v => bs.concat(v)
            )(
                f(a)(x)
            ),
            Tuple(acc)([])
        );


    // nest :: Tree a -> [a]
    const nest = tree => {
        // Allowing for lazy (on-demand) evaluation.
        // If the nest turns out to be a function –
        // rather than a list – that function is applied
        // here to the root, and returns a list.
        const xs = tree.nest;

        return "function" !== typeof xs ? (
            xs
        ) : xs(root(tree));
    };


    // root :: Tree a -> a
    const root = tree =>
        // The value attached to a tree node.
        tree.root;


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        ([x, y]) => [x, f(y)];


    // span :: (a -> Bool) -> [a] -> ([a], [a])
    const span = p =>
        // Longest prefix of xs consisting of elements which
        // all satisfy p, tupled with the remainder of xs.
        xs => {
            const i = xs.findIndex(x => !p(x));

            return -1 !== i ? (
                Tuple(xs.slice(0, i))(
                    xs.slice(i)
                )
            ) : Tuple(xs)([]);
        };


    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
        // The paired members of xs and ys, up to
        // the length of the shorter of the two lists.
        ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => [xs[i], ys[i]]);

    // MAIN --
    return main();
})();

Toggling @toc tags on and off in selected rows:

Expand disclosure triangle to view JS Source – @toc toggle
(() => {
    "use strict";

    // `@toc` tag toggled in selected and non-empty
    // lines of a Bike.app (1.3.1 Preview) outline.
    

    // Rob Trew @2022
    // Ver 0.03

    // main :: IO ()
    const main = () => {
        // ------------------- OPTIONS -------------------
        const tagName = "toc";

        const tagValue = "";

        // ------------------- TOGGLE --------------------
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return doc.exists() ? (() => {
            const
                selectedRows = doc.rows.where({
                    selected: true,
                    _not: [{
                        name: ""
                    }]
                });

            return Boolean(selectedRows.length) ? (() => {
                const
                    tagRegex = new RegExp(
                        `\\s+(@${tagName}\\(.*\\)|@${tagName})`, 
                        "gu"
                    ),
                    isTagged = Boolean(
                        tagRegex.exec(
                            selectedRows.at(0).name()
                        )
                    ),
                    updated = isTagged ? (
                        clearTag(tagRegex)
                    ) : addTag(tagRegex)(tagName)(tagValue);

                return (
                    selectedRows().forEach(
                        row => row.name = updated(
                            row.name()
                        )
                    ),
                    isTagged ? (
                        `@${tagName} cleared`
                    ) : `Tagged @${tagName}`
                );
            })() : "Nothing selected in Bike";
        })() : "No documents open in Bike";
    };

    // ---------------------- TAGS -----------------------

    // clearTag :: Regex -> String -> String
    const clearTag = tagRegex =>
        rowText => rowText.replace(tagRegex, "");


    // addTag :: String -> String -> String -> String
    const addTag = tagRegex =>
        tagName => tagValue => txt => {
            const
                affix = Boolean(tagValue.length) ? (
                    `@${tagName}(${tagValue})`
                ) : `@${tagName}`;

            return `${clearTag(tagRegex)(txt)} ${affix}`;
        };


    // --------------------- GENERIC ---------------------

    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
        .toISOString();


    // taskPaperDateString :: Date -> String
    const taskPaperDateString = dte => {
        const [d, t] = iso8601Local(dte).split("T");

        return [d, t.slice(0, 5)].join(" ");
    };

    // MAIN ---
    return main();
})();
1 Like