Support inquiry: MarginNote

Hi!

In the past year I got hooked on MarginNote which might support most of the requirements for Cognitive Productivity either in its own features, or through its integrations with external tools like e.g. Anki and iThoughtsX.

Will Hook be able to script its way to/from MarginNote?

I expect it won’t be possible from day one, but the developers seem to be so great at developing features that I suppose it shouldn’t be too long until they support the integration API. (Once we discuss that with them.)

Also, I’m very interested in Luc’s opinion of MarginNote. Once I’m back from vacation I could be available to screenshare and discuss about it if there’s interest.

BTW I wasn’t sure which category is appropriate for script requests…

Thank you Daniel.

We’ve had prior requests for integrating with it. But the last time we checked, it doesn’t support automation (required for inter-app communication). So we got in touch with the MarginNote developer earlier this year to request the minimal Apple Script or other capabilities required by Hook (and other automation developers for that matter).

If enough MarginNote customers request it, perhaps they will oblige: https://hookproductivity.com/help/integration/other-app-developers

but we’ll have another look to see if they’ve added anything we can use.If not, we will be back in touch with them, and other apps in our backlog, after the launch, as we extend the range of apps with which Hook out of the box. With Hook coming out of beta with a splash, and the concept of inter-app linking getting traction, I am optimistic.

Re MarginNote: yes, MarginNote’s been on my radar for at least a couple of years. For almost a decade at SFU (me: 2002-2009) I managed the software development of a few substantial integrated learning environment ( statstudy, gStudy and then nStudy, the latter is still active as an academic research tool) with very extensive note-taking capabilities. my co-founder , Brian Shi was the lead developer on the projects for many years. There’s overlap with what MarginNote is doing.

The Hook approach of course is to focus on the core features ( linking and meta-access), allowing users to connect and access items from as many apps as possible.

Thanks for the detailed answer, Luc. I also wrote them here as part of another request for similar integration features.

I was curious about nStudy but concluded that it’s not intended for public usage. Too bad. :slight_smile: I usually try every piece of software that might be promising for my own use.

Good launch! I’m proud of having supported Hook before your launch.

1 Like

I’ve made some scripts for MarginNote. They work when I run them outside of Hook, but when I use them in Hook I get No Linkable Item. Any ideas @LucB?

Get Name

tell application "System Events"
	tell process "MarginNote 3"
		set the clipboard to ""
		if enabled of menu item "Copy" of menu 1 of menu bar item "Edit" of menu bar 1 then
			-- Item selected
			click menu item "Copy" of menu 1 of menu bar item "Edit" of menu bar 1
			delay 0.1
			set theItem to the clipboard
			get item 1 of paragraphs of theItem
		end if
		-- else item not selected
	end tell
end tell

Get Address

tell application "System Events"
	tell process "MarginNote 3"
		set the clipboard to ""
		if enabled of menu item "Copy Note URL" of menu 1 of menu bar item "Edit" of menu bar 1 then
			-- Item selected
			click menu item "Copy Note URL" of menu 1 of menu bar item "Edit" of menu bar 1
			delay 0.1
			get the clipboard
		end if
		-- else item not selected
	end tell
end tell
1 Like

I seem to get a MarginNote3 URL with this Address Script for Hook:

(JavaScript, so the first line needs to be //JavaScript, as below)

//JavaScript
(() => {
    'use strict';

    ObjC.import('AppKit');

    const main = () =>
        either(alert('MarginNote 3 problem'))(
            x => (
                delay(0.2),
                clipboardText()
            )
        )(
            menuItemClickedLR('MarginNote 3')([
                'Edit', 'Copy note URL'
            ])
        );

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


    // clipboardText :: IO () -> String
    const clipboardText = () =>
        // Any plain text in the clipboard.
        ObjC.unwrap(
            $.NSString.alloc.initWithDataEncoding(
                $.NSPasteboard.generalPasteboard
                .dataForType($.NSPasteboardTypeString),
                $.NSUTF8StringEncoding
            )
        );

    // menuItemClickedLR :: String -> [String] -> Either String IO String
    const menuItemClickedLR = strAppName => menuParts => {
        const intMenuPath = menuParts.length;
        return 1 < intMenuPath ? (() => {
            const
                appProcs = Application('System Events')
                .processes.where({
                    name: strAppName
                });
            return 0 < appProcs.length ? (() => {
                Application(strAppName).activate();
                delay(0.1);
                return bindLR(
                    menuParts.slice(1, -1)
                    .reduce(
                        (lra, x) => bindLR(lra)(a => {
                            const menuItem = a.menuItems[x];
                            return menuItem.exists() ? (
                                Right(menuItem.menus[x])
                            ) : Left('Menu item not found: ' + x);
                        })(),
                        (() => {
                            const
                                k = menuParts[0],
                                menu = appProcs[0].menuBars[0]
                                .menus.byName(k);
                            return menu.exists() ? (
                                Right(menu)
                            ) : Left('Menu not found: ' + k);
                        })()
                    )
                )(xs => {
                    const
                        k = menuParts[intMenuPath - 1],
                        items = xs.menuItems,
                        strPath = [strAppName]
                        .concat(menuParts).join(' > ');
                    return bindLR(
                        items[k].exists() ? (
                            Right(items[k])
                        ) : Left('Menu item not found: ' + k)
                    )(x => x.enabled() ? (
                        x.click(),
                        Right('Clicked: ' + strPath)
                    ) : Left(
                        'Menu item disabled : ' + strPath
                    ))
                })
            })() : Left(strAppName + ' not running.');
        })() : Left(
            'MenuItemClickedLR needs a menu path of 2+ items.'
        );
    };


    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // 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 =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl => fr => e =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

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

The problem is just one of timing – the Hook GUI doesn’t give MarginNote 3 enough time for both Address and Name to be collected.

MarginNote turns out to be slow (in a two-punch Copy Address + Copy Name) because of the way it gives leisurely user feedback after each separate copy event. By the time it has done that twice, Hook has already moved on and left poor MarginNote behind.

The JS script below, for example, if launched from something more patient, like Keyboard Maestro, does copy both Address and Name, and returns a Markdown link.

To do that, however, it needs a 0.5s pause for the second stage (copying the Name, after the Address) to allow for the MarginNote GUI to settle down.

(You can reproduce the the problem arising from Hook’s slight impatience, and MarginNote’s leisurely pace, by reducing the second of the two delay entries (see the comment in the source code)).

Hook just needs to adjust its approach to waiting for the other application.

//JavaScript
// Version 2 - copies both Address and Name,
// but needs more time than Hook allows.
(() => {
    'use strict';

    ObjC.import('AppKit');

    // main :: IO ()
    const main = () =>
        either(alert('Hook for MarginNote 3'))(x => x)(
            bindLR(
                menuItemClickedLR('MarginNote 3')([
                    'Edit', 'Copy note URL'
                ])
            )(_ => {
                const strAddr = (
                    delay(0.2),
                    clipboardText()
                );
                return bindLR(
                    strAddr.startsWith('marginnote') ? (
                        Right(strAddr)
                    ) : Left('No address copied')
                )(address => bindLR(
                    menuItemClickedLR('MarginNote 3')([
                        'Edit', 'Copy'
                    ])
                )(_ => {
                    const strName = (
                        delay(0.5), // ADJUST HERE ...
                        clipboardText()
                    );
                    return !strName.startsWith('marginnote') ? (
                        Right('[' +
                            strName.split(/\n/)[0] +
                            '](' + address + ')')
                    ) : Left('No Name copied - try increasing delay ...')
                }))
            })
        );

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


    // clipboardText :: IO () -> String
    const clipboardText = () =>
        // Any plain text in the clipboard.
        ObjC.unwrap(
            $.NSString.alloc.initWithDataEncoding(
                $.NSPasteboard.generalPasteboard
                .dataForType($.NSPasteboardTypeString),
                $.NSUTF8StringEncoding
            )
        );

    // menuItemClickedLR :: String -> [String] -> Either String IO String
    const menuItemClickedLR = strAppName => menuParts => {
        const intMenuPath = menuParts.length;
        return 1 < intMenuPath ? (() => {
            const
                appProcs = Application('System Events')
                .processes.where({
                    name: strAppName
                });
            return 0 < appProcs.length ? (() => {
                Application(strAppName).activate();
                delay(0.1);
                return bindLR(
                    menuParts.slice(1, -1)
                    .reduce(
                        (lra, x) => bindLR(lra)(a => {
                            const menuItem = a.menuItems[x];
                            return menuItem.exists() ? (
                                Right(menuItem.menus[x])
                            ) : Left('Menu item not found: ' + x);
                        })(),
                        (() => {
                            const
                                k = menuParts[0],
                                menu = appProcs[0].menuBars[0]
                                .menus.byName(k);
                            return menu.exists() ? (
                                Right(menu)
                            ) : Left('Menu not found: ' + k);
                        })()
                    )
                )(xs => {
                    const
                        k = menuParts[intMenuPath - 1],
                        items = xs.menuItems,
                        strPath = [strAppName]
                        .concat(menuParts).join(' > ');
                    return bindLR(
                        items[k].exists() ? (
                            Right(items[k])
                        ) : Left('Menu item not found: ' + k)
                    )(x => x.enabled() ? (
                        x.click(),
                        Right('Clicked: ' + strPath)
                    ) : Left(
                        'Menu item disabled : ' + strPath
                    ))
                })
            })() : Left(strAppName + ' not running.');
        })() : Left(
            'MenuItemClickedLR needs a menu path of 2+ items.'
        );
    };


    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // 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 =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl => fr => e =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

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

I’ve logged an issue on our side to look at this (some time after 1.3 is released).

Regarding MarginNote: When the dust settles here, we’ll add MarginNote scripts.

Thank you all for developing this support. MarginNote is a very interesting application. (At SFU, the gStudy and nStudy software 3 of us here at CogSci Apps worked on [myself as s/w lead from 2002-2009] was similar in philosophy, i.e., an integrated environment for delving, note-taking, concept mapping, chat tool, writing documents, etc.)

1 Like

Hook just needs to adjust its approach to waiting for the other application.

Perhaps it just needs a list of a few slow performers :snail:

Maybe there could be an additional / configurable boolean or int associated with each script. Maybe eventually Hook itself could dynamically figure out what the value/number should be.

1 Like

I’m sure experiment will find something good.

In the meanwhile, a Keyboard Maestro macro:

2 Likes

Curiously enough, that second JavaScript draft now seems to be working fine from Hook on my system …

Perhaps worth trying on other systems too …

(It’s a combined Name and Address script – try leaving the Name script empty, and pasting the code linked to below in the Address script panel)

1 Like

That works, many thanks @RobTrew :grinning_face_with_smiling_eyes:

1 Like

Scripts bundle 84 contains the Margin Note updates. Than you @RobTrew and @stevelw ! 1.3 coming up …

1 Like

Hi! What do I need to be doing in MarginNote for Hook (1.4) not to say “No linkable item found in MarginNote 3”?

It’s working here, and assuming that you have the latest copy of the scripts,

e.g. Preferences > Updates > Check now in English-language macOS installation,

then my first guess is (if your installation is not using any variety of Anglo Saxon as the UI language) that the problem is rooted in the use of GUI scripting, and an assumption, in the Hook script for MarginNote, of English-language labels for menu items:

So, for example, in the Preferences > Scripts entry for your version of MarginNote, where you will find two scripts:

  • Get Name
  • Get Address

You could, in the short term, edit the two scripts so that Anglo-Saxon menu path labels like:

  • Edit > Copy

  • Edit > Copy Note URL

are replaced by locally more relevant terms.

More generally, I’m not sure whether the makers of Hook have developed an approach to localisation issues in these scripts yet.

Thanks for the detailed answer, @RobTrew!

image

If capitalization is important, I see “note” differs from “Note”.

Apart from that, I realize my issue might be related to my using SetApp’s version of MN3.
I could take a look at the other scripts in Hook where two different scripts are provided, one for non-SetApp, one for SetApp version of the same app.

Thanks again!

Nope. I use SetApp’s instance of MN3 and it works great with Hook.

I’m going to post (again) this likely culprit, which seems to bedevil other Hook users:

Replace “Chrome” with “MarginNote”/

Thanks; you’ve eliminated two possible causes but I’ve still got the issue.
Now I know that SetApp’s MN is supported, and I confirm that all the automation privacy checkboxes are checked, incuding Hook’s request for control of MarginNote. :confused:

Sorry the interaction with MarginNote is not working for you, @danieljomphe. I’ve asked our MarginNote interaction dev to look into this.

@danieljomphe , Bi Ling here wrote to me saying

It works fine on my machine.

One thing that I noticed that is when select a node, it can’t be in an edit mode. If in edit mode, “Copy note url” menu item is disabled. Hook would not work

Can you please ask him to give us a screen shot with Hook window on a select note? Also before invoke Hook window, ask him to check Edit ->Copy Note URL is enabled or not. That might give us some clue.