Bugged for me or can we add expand/collapse memory?

Currently I love using bike on my Mac. (I’d love to be able to use a simple version on iPhone or tablet!)

One dumb little thing but it’s annoying enough that it actually stops me from using it often, is that each time I open a document all of the levels are expanded. I’m not sure if it’s just bugged for me or if memory of what was collapsed or expanded is able to be saved in the future.

It drives me crazy, especially as I have a few rather large docs, having to collapse everything first as it seems to expand every single section, rather then just go the the area of the doc I’d like to edit and start working. I’d happily put some money towards that being added if it helps.

There’s some discussion of that question here:

Saving and restoring the collapse/expansion state? - Bike - Hog Bay Software Support

In particular:

For that to work you need to make sure that you don’t have System > General > “Close Windows when quitting an app” selected.

1 Like

Ah. Yea doing that will break ways I use other apps though. A bit of a deal killer sadly. I guess the alternative is just breaking everything down into smaller files or going to far into the weeds for me at this point sadly to use for me as someone not very technical for a simple notes app. :face_with_diagonal_mouth:

I know what you mean – I happen to like the way that System > General > Close Windows when quitting an app protects me from having too many code-editor scratch-file windows lying around.

(It would be good to have per-app settings for that, in macOS)

Not sure what future plans are.

(In the meanwhile I might experiment to see if there’s a feasible scripted solution to persistent memory of fold-states.

Perhaps something like – restore last remembered fold-state for this doc ?)

1 Like

Yes. That kind of thing would be so awesome! I love how fast and just otherwise awesome this is for my notes and just organizing thoughts quickly. That one thing is just super annoying with my most used docs that tend to be very large with many grouping like that.

@FutureNathan do you use Keyboard Maestro, FastScripts 3, or Alfred - Productivity App for macOS ?

I’ve sketched rough drafts of a pair of experimental scripts:

{Save fold state, restore fold state}

(just for testing with dummy files, for the moment, and we might need to ask @jessegrosjean, when he gets back, whether it’s sensible to be storing custom attributes in the Bike file itself, or whether we should really be keeping the state elsewhere)


Experimental drafts for testing with dummy data only:
SaveRestoreBikeFoldFocus.kmmacros.zip (4.9 KB)


Expand disclosure triangle to view script: Save Fold and Focus State
(() => {
    "use strict";

    // Save fold state of front document as custom attribute
    // custom.foldState :: JSON Array of folded row ids

    // Rob Trew @2022
    // EXPERIMENTAL DRAFT – USE WITH DUMMY DATA ONLY

    // Ver 0.02

    // main :: IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return doc.exists() ? (() => {
            const
                fp = `${doc.file()}`,
                collapsedIdsJSON = JSON.stringify(
                    doc.rows.where({
                        collapsed: true
                    }).id()
                ),
                maybeFocusedRow = doc.focusedRow(),
                kvs = [
                    [
                        "custom.foldState",
                        `'${collapsedIdsJSON}'`
                    ],
                    [
                        "custom.focusRow",
                        Boolean(maybeFocusedRow) ? (
                            maybeFocusedRow.id()
                        ) : "''"
                    ]
                ];

            return either(
                alert("Save fold state")
            )(x => x)(
                fileAttrsSetLR(filePath(fp))(kvs)
            );
        })() : "No documents open in Bike";
    };

    // fileAttrSetLR :: FilePath ->
    // [(String, String)] -> Either String  Dict
    const fileAttrsSetLR = fp =>
        kvs => {
            const fpFull = filePath(fp);

            return bindLR(
                doesFileExist(fpFull) ? (
                    Right(fpFull)
                ) : Left(`File not found: ${fpFull}`)
            )(fpFound => {
                try {
                    Object.assign(
                        Application.currentApplication(),
                        {includeStandardAdditions: true}
                    )
                    .doShellScript(
                        kvs.map(
                            ([k, v]) =>
                                `xattr -w ${k} ${v} "${fpFound}"`
                        ).join("\n")
                    );

                    return Right(
                        [
                            kvs.map(
                                ([k, v]) => `${k}:${v}`
                            ).join("\n"),
                            fpFound
                        ].join("\n")
                    );
                } catch (e) {
                    return Left(e.message);
                }
            });
        };

    // ----------------------- 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
            );
        };

    // 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
        );

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
        .fileExistsAtPathIsDirectory(
            $(fp)
            .stringByStandardizingPath, ref
        ) && !ref[0];
    };

    // --------------------- 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);

    // 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);


    // main :: IO()
    return main();
})();
Expand disclosure triangle to view script: Restore Fold and Focus
(() => {
    "use strict";

    // Restore fold and focus state of front document
    // from custom file attributes
    // custom.foldState :: JSON Array of any folded row ids
    // custom.focusRow :: id of focus row (if any)

    // Rob Trew @2022
    // EXPERIMENTAL DRAFT – USE WITH DUMMY DATA ONLY

    // Ver 0.03

    // main :: IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return doc.exists() ? (
            either(
                alert("Restore fold and focus state")
            )(
                restoredFoldsAndFocus(doc)
            )(
                fileAttrsReadLR(
                    filePath(`${doc.file()}`)
                )([
                    "custom.foldState",
                    "custom.focusRow"
                ])
            )
        ) : "No documents open in Bike";
    };


    // ----------------------- 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
            );
        };


    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = 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
        );


    // fileAttrsReadLR :: FilePath -> String -> Maybe Dict
    const fileAttrsReadLR = fp =>
        ks => {
            const fpFull = filePath(fp);

            return "null" !== fpFull ? (
                bindLR(
                    doesFileExist(fpFull) ? (
                        Right(fpFull)
                    ) : Left(`File not found: ${fpFull}`)
                )(fpFound => {
                    try {
                        return Right(
                            zip(ks)(lines(
                                Object.assign(
                                    Application.currentApplication(),
                                    {includeStandardAdditions: true}
                                )
                                .doShellScript(
                                    ks.map(
                                        k => `xattr -p ${k} "${fpFound}"`
                                    ).join("\n")
                                )
                            ))
                            .reduce(
                                (a, [k, v]) => Object.assign(
                                    {[k]: v},
                                    a
                                ),
                                {}
                            )
                        );
                    // );
                    } catch (e) {
                        return Left(e.message);
                    }
                })
            ) : Left("Front document not saved.");
        };

    // restoredFoldsAndFocus ::
    // Bike Doc Dict -> IO String
    const restoredFoldsAndFocus = doc =>
        dict => {
            const
                foldedIDs = either(() => [])(x => x)(
                    jsonParseLR(dict["custom.foldState"])
                ),
                focusRowId = dict["custom.focusRow"] || "",
                rows = doc.rows,

                focusMessage = Boolean(focusRowId) ? (
                    restoreFocus(doc)(focusRowId)
                ) : "",
                foldMessage = (() => {
                    rows.where({containsRows: true})()
                    .forEach(x => x.expand());

                    return foldedIDs.flatMap(k => {
                        const row = rows.byId(k);

                        return row.exists() ? [(
                            row.collapse(),
                            row.name()
                        )] : [];
                    }).join();
                })();

            return [
                `Restored folds:\n${foldMessage}`,
                focusMessage
            ].join("\n");
        };

    // restoreFocus :: Bike Rows -> String -> Bike IO ()
    const restoreFocus = doc =>
        rowID => {
            const row = doc.rows.byId(rowID);

            return row.exists() ? (
                doc.focusedRow = row,
                `Focus restored: '${row.name()}'`
            ) : `Focus row not found: ${rowID}`;
        };


    // --------------------- 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);

    // 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);

    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                [
                    e.message,
                    `(line:${e.line} col:${e.column})`
                ].join("\n")
            );
        }
    };

    // lines :: String -> [String]
    const lines = s =>
    // A list of strings derived from a single string
    // which is delimited by \n or by \r\n or \r.
        Boolean(s.length) ? (
            s.split(/\r\n|\n|\r/u)
        ) : [];


    // 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]]);

    // showLog :: a -> IO ()
    const showLog = (...args) =>
    // eslint-disable-next-line no-console
        console.log(
            args
            .map(JSON.stringify)
            .join(" -> ")
        );

    // main :: IO()
    return main();
})();

@jessegrosjean I wonder if something is fractionally out of place (build 69) in the typing of the expand and collapse methods ?

Perhaps list ⇄ collection reference ?

The dictionary entry says that these methods expect lists of rows, but both give type error messages in AppleScript when a list of rows is passed to them, though they do work with collection references.

Screenshot 2022-08-26 at 16.26.29

Screenshot 2022-08-26 at 16.27.38


In Javascript they seem, at the moment (preview 69) to be working neither with lists (JS Arrays) nor with collection references – both types of argument trip type error messages, and produce no effect)

Expand disclosure triangle to view AppleScript test snippet
tell application "Bike"
    tell front document
        set refRows to a reference to its rows
        set rowList to its rows as list
        
        log (class of refRows is list) -- false
        log (class of rowList is list) -- true
        
        -- both succeed, anomalously (a dynamic type conversion somewhere ?)
        collapse refRows
        expand refRows
        
        -- both fail, unexpectedly (a type declaration glitch in the API ?)
        collapse rowList
        expand rowList
    end tell
end tell


-- (All permutations failing with the more strictly typed 
--   `osascript -l JavaScript`)

I notice that OmniOutliner defines .collapsed and .expanded as R/W properties. I don’t know if making them writable looks feasible in the Bike context ?

1 Like

I think I can fix, but I don’t really know what’s going on…

First I’m using a command instead of property so that there’s the option to include the all parameter. I think I would like to keep doing this.

As to why references work and the other case does not… AppleScript only stays in my brain very temporarily so I can’t exactly say why it’s happening. I think it must have to do with fact that rows are supposed to be a direct parameter to the command. In my mind (I don’t really know that this is correct) the standard way to use the direct parameter collapse command would be like this (seems to work):

tell application "Bike"
	tell front document
		tell rows
			collapse
		end tell
	end tell
end tell

I guess when you use references like:

collapse refRows

It can be interpreted in that standard way, and maybe not for when you don’t use references. Really it’s all a mystery to me. Here’s what I’m seeing on my AppKit side:

case: set refRows to a reference to rows

Bike Suite.collapse
	Direct Parameter: <NSPropertySpecifier: rows of orderedDocuments 1>
	Receivers: <NSPropertySpecifier: rows of orderedDocuments 1>
	Arguments:     {
    }

case: set rowList to rows as list

Bike Suite.collapse
	Direct Parameter: (
    "<NSUniqueIDSpecifier: rows with an ID of \"VH\" of orderedDocuments named \"Untitled\">",
    "<NSUniqueIDSpecifier: rows with an ID of \"lJ\" of orderedDocuments named \"Untitled\">",
    "<NSUniqueIDSpecifier: rows with an ID of \"vZ\" of orderedDocuments named \"Untitled\">"
)
	Receivers: (null)
	Arguments:     {
        "" =         (
            "<NSUniqueIDSpecifier: rows with an ID of \"VH\" of orderedDocuments named \"Untitled\">",
            "<NSUniqueIDSpecifier: rows with an ID of \"lJ\" of orderedDocuments named \"Untitled\">",
            "<NSUniqueIDSpecifier: rows with an ID of \"vZ\" of orderedDocuments named \"Untitled\">"
        );
    }

I don’t really know why they come through as receivers in one case and arguments with “” key in other. I will make the code that unpacks them just check for both, and I think that will make cases work, though I’m not sure if it’s really correct.

1 Like

Not technical to understand these last comments but ahh so glad people are looking at it! I really love how easy bike is and this would make it so much more useful. Thanks!

1 Like