Bike 1.0 Dev (34)

  • Added “Standard” and “Minimal” handle style preferences
  • Added Command-Click on handle to push/pop focus instead of expand/collapse
  • Changed default row spacing back to 1.0
  • Changed text caret size to match text size changes
  • Changed file:// links to reveal in Finder instead of open because of sandbox restrictions
  • Changed preference pane sliders to have immediate effect on outline
  • Changed selection to better handle case where selection is hidden when item is collapsed
  • Fixed cut command now works when in outline mode

Two reversals here: handles default to a more standard display, and changed default row spacing back to 1.0.

The handle change was just a few lines of code and at the moment I’m liking it better. I still find it more distracting, and previous handle styling is available via preferences, but for now watching the handles rotate is more fun than reading my notes anyway. Also makes the app feel a bit more fancy, go sales!

Second I reverted default row spacing back to 1.0, so there is now same spacing between rows as there is between any line of wrapped text. Still not sure one this one, it makes viewing a document with lots of text wrapping quite a bit harder to visually parse. But outlines with lots of single line items look nicer. I normally have single line items so optimizing for that today.

2 Likes

I just had an instance where launching Bike seemed delayed by a few seconds. Slow. I repeated the behavior a few times. Then closed some apps (I had a lot open) and the problem went away. Not sure what caused the problem, and now I can’t reproduce it. Anyway if you see similar problem please let me know, Bike should open pretty much instantly unless you have a really huge outline.

2 Likes

Very happy with the rotating handles!

2 Likes

For cases where opening the local link in an app is useful, this Keyboard Maestro macro still works:

Follow first link in selected Bike item.kmmacros.zip (4.0 KB)


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

    ObjC.import("AppKit");

    // Rough interim script for:
    // extracting first link from the clipboard
    // supplying a document id if it's a bike:// link
    // without one, and opening the link through the shell

    // Draft 0.00  Rob Trew @2022

    // main :: IO ()
    const main = () =>
        either(
            alert("Open link from Bike")
        )(
            url => (
                Object.assign(
                    Application.currentApplication(), {
                        includeStandardAdditions: true
                    }
                ).doShellScript(`open ${url}`),
                url
            )
        )(
            bindLR(
                clipTextLR()
            )(txt => bindLR(
                firstLinkFoundLR(txt)
            )(url => url.startsWith(
                "bike://#"
            ) ? (
                bindLR(
                    filePathFromFrontWindowLR()
                )(fp => bindLR(
                    readFileLR(fp)
                )(xml => bindLR(
                    xmlDocFromStringLR(xml)
                )(doc => bindLR(
                    bikeDocIdFromXmlDocLR(doc)
                )(id => Right(
                    `bike://${id}${url.slice(7)}`
                )))))
            ) : Right(url)))
        );

    // --------------- FIRST LINK IN TEXT ----------------

    // firstLinkFoundLR :: String -> Either String URI
    const firstLinkFoundLR = s => {
        // Either a message or the first URI
        // found in the given string.
        const parts = s.split("://");

        return 1 < parts.length ? (() => {
            const [as, bs] = parts.map(words);
            const
                a = 0 < as.length ? (
                    last(as)
                ) : "",
                b = 0 < bs.length ? (
                    bs[0]
                ) : "";

            const
                scheme = last(a.split(/\b/u)),
                resource = takeWhile(
                    c => "!#$&'*+,/:;=?@[]".includes(s) || (
                        !(/[\s()]/u).test(c)
                    )
                )([...b]).join("");

            return Boolean(scheme) ? (
                Boolean(resource) ? (
                    Right(`${scheme}://${resource}`)
                ) : Left(`No resource found: ${scheme}://`)
            ) : Left(`No scheme found: ://${resource}`);
        })() : Left("No link found in string.");
    };

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


    // bikeDocIDFromXmlDoc :: NSXMLDoc ->
    // Either String String
    const bikeDocIdFromXmlDocLR = doc => {
        // Either a message or the ID string of the root
        // <ul> tag of a Bike document parsed as XML.
        const
            uw = ObjC.unwrap,
            e = $(),
            rootUL = uw(
                doc.nodesForXPathError("//body/ul/@id", e)
            )[0];

        return rootUL.isNil() ? (
            Left(uw(e.localizedDescription))
        ) : Right(
            // uw(rootUL.attributeForName("id").stringValue)
            uw(rootUL.stringValue)
        );
    };


    // filePathFromFrontWindowLR  :: () -> Either String FilePath
    const filePathFromFrontWindowLR = () => {
        // ObjC.import ('AppKit')
        const
            appName = ObjC.unwrap(
                $.NSWorkspace.sharedWorkspace
                .frontmostApplication.localizedName
            ),
            ws = Application("System Events")
            .applicationProcesses.byName(appName).windows;

        return bindLR(
            0 < ws.length ? Right(
                ws.at(0).attributes.byName("AXDocument")
                .value()
            ) : Left(
                `No document windows open in ${appName}.`
            )
        )(
            docURL => null !== docURL ? (
                Right(decodeURIComponent(docURL.slice(7)))
            ) : Left(
                `No saved document active in ${appName}.`
            )
        );
    };


    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        // Either a message, (if no clip text is found),
        // or the string contents of the clipboard.
        const
            v = ObjC.unwrap(
                $.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString)
            );

        return Boolean(v) && 0 < v.length ? (
            Right(v)
        ) : Left("No utf8-plain-text found in clipboard.");
    };


    // xmlDocFromStringLR ::
    // XML String -> Either String NSXMLDocument
    const xmlDocFromStringLR = xml => {
        const
            error = $(),
            node = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                xml, 0, error
            );

        return node.isNil() ? (
            Left("File could not be parsed as XML")
        ) : Right(node);
    };


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


    // last :: [a] -> a
    const last = xs =>
        // The last item of a list.
        0 < xs.length ? (
            xs.slice(-1)[0]
        ) : null;


    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );

        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };


    // takeWhile :: (a -> Bool) -> [a] -> [a]
    const takeWhile = p =>
        xs => {
            const i = xs.findIndex(x => !p(x));

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


    // words :: String -> [String]
    const words = s =>
        // List of space-delimited sub-strings.
        s.split(/\s+/u);

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

1 Like

I’d like it if there were a “Restore Defaults” button in Preferences for the text size, line height, and row spacing settings, but given the current simple layout of the Prefs window, I’m not sure how you could do that while making it clear the user is only restoring the display settings to defaults.

Or, maybe, what if as you move the sliders around for those settings, there were some indication of which value is the default?

Also, FWIW, I just trashed my com.hogbaysoftware.Bike.plist file in Bike’s container and the new default for Row Height seems to be 1.5, not 1.0.

I’ve just been adding some color settings, and they need reset even more. I’m doing them like this:

I think I can take a similar approach. Group all the text settings under “Layout:” and then put reset button at bottom. I also might replace sliders with text fields and stepper buttons. I think that would be cleaner if maybe a little less fun.