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

@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();
})();
1 Like