Help debugging this script?

No changes desired on this. I am thrilled with this part as is. :slight_smile:

Rob—this is great. It is helping me gain insight into how JavaScript parses code.

The (0) part points to the first tab, right? And what I want is the frontmost, or current, tab.

This made me think, then analyze, then research. “.” is sort of like “of” in AppleScript, right?

I took [tabs.at(0).name()] and adjusted it to:

[windows[0].currentTab.name()]

…and that seems to work well.

I am not sure why windows[0] needs to be there, but the code fails without it.

One oddity: If I run the script with Safari as the active app, the script works as expected. However, if Safari is hidden, and has multiple windows open, the tab that is grabbed is from a different window. Does the window index renumber if Safari is hidden?

Perfect! You rock!

So, do you have an Amazon wishlist? If you don’t want to post it publicly, please DM me.

The current iteration, which is now in active use in my workflow, is:

(() => {
    "use strict";

    const
        fpMobile = "~/Library/Mobile Documents/",
        fpWorkflows = "iCloud~is~workflow~my~workflows/",
        fpLinks = "Documents/Links.txt",
        fp = `${fpMobile}${fpWorkflows}${fpLinks}`,
        strFullPath = ObjC.unwrap(
            $(fp).stringByExpandingTildeInPath
        );

    const
        tp = Application("TaskPaper"),
        doc = tp.open(Path(strFullPath));


    // main :: IO ()
    const main = () => {
        const windows = Application("Safari").windows;

        return either(
            // Left channel message in case of a problem.
            msg => alert(
                "Copy Safari tab list to TaskPaper"
            )(msg)
        )(
            // Simple string value returned if all is well.
            x => x
        )(
            bindLR(
                0 < windows.length ? (
                    Right(windows.at(0).tabs)
                ) : Left("No windows open in Safari")
            )(tabs => {
                const
                    dateString = taskPaperDateString(
                        new Date()
                    ).slice(0, 10),
                    linkStrings = zipWith(
                        name => link =>
                            `- ${name} → ${link} @link(${dateString})`
                    )(
					[windows[0].currentTab.name()]

                    )(
					[windows[0].currentTab.url()]
                    );

                const result = doc.evaluate({
                    script: `${TaskPaperContext}`,
                    withOptions: {
                        projectName: "Links",
                        linkTexts: linkStrings
                    }
                });

                return Right(
                    (doc.save(), result)
                );
            })
        );
    };

    // -------------------- TASKPAPER --------------------

    // TaskPaperContext :: Editor -> Dict -> String
    const TaskPaperContext = (editor, options) => {
        const tp3Main = () => {
            const
                outline = editor.outline,
                xs = options.linkTexts,
			  target = outline.root;

            outline.groupUndoAndChanges(() => {
                const children = xs.map(
                    txt => outline.createItem(txt)
                );

                target.insertChildrenBefore(
                    children, target.firstChild
                );
            });

            return xs.join("\n");
        };

        // projectFoundOrCreated :: TP3Outline ->
        // String -> Bool -> TP3Item
        const projectFoundOrCreated = outline =>
            // A reference to a TaskPaper Project item,
            // either found in the outline, or created
            // at the top of it.
            projectName => {
                const
                    k = projectName.trim(),
                    matches = outline.evaluateItemPath(
                        `//project "${k}"`
                    ),
                    blnFound = 0 < matches.length,
                    project = blnFound ? (
                        matches[0]
                    ) : outline.createItem(`${k}:`);

                return (
                    blnFound || outline.insertItemsBefore(
                        project,
                        outline.root.firstChild
                    ),
                    project
                );
            };

        return tp3Main();
    };

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

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

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

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );

    return main();
})();

I think the window indexing expresses the focus layering.

(Foremost → 0)

( Many thanks for kind wishlist questions, but no need : )

PS:

"windows[0]" gets pre-processed back, at run-time, to windows.at(0), which you can write directly, if you want to bypass that string munging.

(The problem with the windows[0] rewrite is that it’s just a cosmetic conceit, and can lull people into thinking that a collection like Application.windows is, and will behave like, a JS Array.
It ain’t, and won’t, alas : )

1 Like

Fair enough. Sending a virtual handshake, high five or hug your way. :slight_smile:

I will use these a lot every day. Your help has been, is, and will always be appreciated. Kudos!

Bypassing string munging sounds like a fine idea. I have just updated my code.

New idea for a third version:

There are times when I want to add some text to the line, to remind myself why I captured the title and URL.

Can JavaScript capture text from the Mac clipboard and insert it?

Example text on the clipboard: Check if Apple has approved this

Example desired result:

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

    ObjC.import("AppKit");

    const main = () =>
        clipText();

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

    // clipText :: () -> IO String
    const clipText = () => {
    // Either a message, or the plain text
    // content of the clipboard.
        const
            maybeClip = $.NSPasteboard.generalPasteboard
            .stringForType($.NSPasteboardTypeString);

        return maybeClip.isNil() ? (
            "No utf8-plain-text found in clipboard."
        ) : ObjC.unwrap(maybeClip);
    };

    return main();
})();
1 Like

or, for composition with other Left | Right values:

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

    ObjC.import("AppKit");

    // Rob Trew 2022

    const main = () =>
        either(
            alert("Text content of clipboard")
        )(
            clipText => clipText
        )(
            clipTextLR()
        );

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


    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
    // Either a message, or the plain text
    // content of the clipboard.
        const
            mb = $.NSPasteboard.generalPasteboard
            .stringForType($.NSPasteboardTypeString);

        return mb.isNil() ? (
            Left("No utf8-plain-text found in clipboard.")
        ) : Right(ObjC.unwrap(mb));
    };


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


    return main();
})();
1 Like

@complexpoint — I’ve been trying to combine your Mac clipboard code, and all attempts thus far have failed.

Can you point me in the right direction?

Can you show us what you’ve tried,
and what message or unintended result your are getting ?

Tried several tactics.

My current attempt results in: Error: Connection is invalid.

(() => {
    "use strict";

    ObjC.import("AppKit");

    const
        fpMobile = "~/Library/Mobile Documents/",
        fpWorkflows = "iCloud~is~workflow~my~workflows/",
        fpLinks = "Documents/Links.txt",
        fp = `${fpMobile}${fpWorkflows}${fpLinks}`,
        strFullPath = ObjC.unwrap(
            $(fp).stringByExpandingTildeInPath
        );

    const
        tp = Application("TaskPaper"),
        doc = tp.open(Path(strFullPath));

    const mcb = () =>
        either(
            alert("Text content of clipboard")
        )(
            clipText => clipText
        )(
            clipTextLR()
        );

    // main :: IO ()
    const main = () => {
        const windows = Application("Safari").windows;

        return either(
            // Left channel message in case of a problem.
            msg => alert(
                "Copy Safari tab list to TaskPaper"
            )(msg)
        )(
            // Simple string value returned if all is well.
            x => x
        )(
            bindLR(
                0 < windows.length ? (
                    Right(windows.at(0).tabs)
                ) : Left("No windows open in Safari")
            )(tabs => {
                const
                    dateString = taskPaperDateString(
                        new Date()
                    ).slice(0, 10),
                    linkStrings = zipWith(
                        name => link =>
                            `- ${mcb} → ${name} → ${link} @link(${dateString})`
                    )(
					[windows.at(0).currentTab.name()]

                    )(
					[windows.at(0).currentTab.url()]
                    );

                const result = doc.evaluate({
                    script: `${TaskPaperContext}`,
                    withOptions: {
                        projectName: "Links",
                        linkTexts: linkStrings
                    }
                });

                return Right(
                    (doc.save(), result)
                );
            })
        );
    };

    // -------------------- TASKPAPER --------------------

    // TaskPaperContext :: Editor -> Dict -> String
    const TaskPaperContext = (editor, options) => {
        const tp3Main = () => {
            const
                outline = editor.outline,
                xs = options.linkTexts,
			  target = outline.root;

            outline.groupUndoAndChanges(() => {
                const children = xs.map(
                    txt => outline.createItem(txt)
                );

                target.insertChildrenBefore(
                    children, target.firstChild
                );
            });

            return xs.join("\n");
        };

        // projectFoundOrCreated :: TP3Outline ->
        // String -> Bool -> TP3Item
        const projectFoundOrCreated = outline =>
            // A reference to a TaskPaper Project item,
            // either found in the outline, or created
            // at the top of it.
            projectName => {
                const
                    k = projectName.trim(),
                    matches = outline.evaluateItemPath(
                        `//project "${k}"`
                    ),
                    blnFound = 0 < matches.length,
                    project = blnFound ? (
                        matches[0]
                    ) : outline.createItem(`${k}:`);

                return (
                    blnFound || outline.insertItemsBefore(
                        project,
                        outline.root.firstChild
                    ),
                    project
                );
            };

        return tp3Main();
    };

    // ----------------------- 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
            );
        };
		
	// clipTextLR :: () -> Either String String
    const clipTextLR = () => {
    // Either a message, or the plain text
    // content of the clipboard.
        const
            mb = $.NSPasteboard.generalPasteboard
            .stringForType($.NSPasteboardTypeString);

        return mb.isNil() ? (
            Left("No utf8-plain-text found in clipboard.")
        ) : Right(ObjC.unwrap(mb));
    };

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

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

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );

    return main();
})();

One thing that jumps to the eye is that in your template string:

- ${mcb} → ${name} → ${link} @link(${dateString})

mcb is the name of a function, but to call that function and derive a String value from it would require a trailing (), i.e. mcbmcb()

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

    ObjC.import("AppKit");

    // @Jim 2022
    // Minor edit by @RobTrew 2022
    // Ver 0.02

    // main :: IO ()
    const main = () => {
        const
            fpMobile = "~/Library/Mobile Documents/",
            fpWorkflows = "iCloud~is~workflow~my~workflows/",
            fpLinks = "Documents/Links.txt",
            fp = `${fpMobile}${fpWorkflows}${fpLinks}`,
            // fp = "~/Desktop/Links.txt",
            fpFullPath = ObjC.unwrap(
                $(fp).stringByExpandingTildeInPath
            );

        return doesFileExist(fpFullPath) ? (() => {
            const
                tp = Application("TaskPaper"),
                windows = Application("Safari").windows,
                doc = tp.open(Path(fpFullPath));

            return either(
            // Left channel message in case of a problem.
                alert("Copy Safari tab list to TaskPaper")
            )(
            // Simple string value returned if all is well.
                x => x
            )(
                bindLR(
                    0 < windows.length ? (
                        Right(windows.at(0))
                    ) : Left("No windows open in Safari")
                )(
                    linkDocUpdatedLR(doc)
                )
            );
        })() : `File not found: ${fpFullPath}`;
    };

    // linkDocUpdatedLR :: Taskpaper Document ->
    // Safari Window -> IO String
    const linkDocUpdatedLR = doc =>
        win => {
            const
                dateString = taskPaperDateString(
                    new Date()
                ).slice(0, 10),
                tag = `@link(${dateString})`,
                currentTab = win.currentTab,
                linkTexts = zipWith(
                    name => link =>
                        `- ${mcb()} → ${name} → ${link} ${tag}`
                )(
                    [currentTab.name()]

                )(
                    [currentTab.url()]
                );

            const result = doc.evaluate({
                script: `${TaskPaperContext}`,
                withOptions: {linkTexts}
            });

            return Right(
                (doc.save(), result)
            );
        };

    // -------------------- TASKPAPER --------------------

    // TaskPaperContext :: Editor -> Dict -> String
    const TaskPaperContext = (editor, options) => {
        const
            outline = editor.outline,
            xs = options.linkTexts,
            target = outline.root;

        outline.groupUndoAndChanges(() => {
            const children = xs.map(
                txt => outline.createItem(txt)
            );

            target.insertChildrenBefore(
                children, target.firstChild
            );
        });

        return xs.join("\n");
    };

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

    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
    // Either a message, or the plain text
    // content of the clipboard.
        const
            mb = $.NSPasteboard.generalPasteboard
            .stringForType($.NSPasteboardTypeString);

        return mb.isNil() ? (
            Left("No utf8-plain-text found in clipboard.")
        ) : Right(ObjC.unwrap(mb));
    };

    const mcb = () =>
        either(
            alert("Text content of clipboard")
        )(
            clipText => clipText
        )(
            clipTextLR()
        );

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

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

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );

    return main();
})();
1 Like

Thank you for lending your eye!

Kudos!