Save Chosen Clipboard Flavour (text or image) to file

  • Lists flavors (NSPasteboard types) found in the system clipboard,
  • saves contents of chosen type (text or image) to file.


Save Chosen Clipboard Flavor to File.kmmacros (19 KB)

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

    ObjC.import('AppKit');

    // Menu of Pasteboard Types found in system clipboard
    // at a given changeCount (incremented whenever the
    // the clipboard is updated) and with a time-stamp.

    // Rob Trew @2025
    // Ver 0.01

    // ---------------------- MAIN -----------------------
    // main :: IO ()
    const main = () => {
        const [changeCount, kvs] = clipFlavourMenu();

        return {
            length: kvs.length,
            menu: kvs.map(fst).join("\n"),
            dict: Object.fromEntries(kvs),
            time: dateTimeString(new Date()),
            // Changes each time clipboard is updated
            changeCount
        };
    };


    // clipFlavourMenu() :: IO () -> [String]
    const clipFlavourMenu = () => {
        const [changeCount, clipTypes] = typesNowInClipboard();

        return Tuple(changeCount)(
            sortOn(fst)(
                clipTypes.flatMap(
                    nameAndUTIunlessNoise
                )
            )
        );
    };


    // nameAndUTIunlessNoise :: UTI -> [(String, UTI)]
    const nameAndUTIunlessNoise = uti =>
        !(
            uti.startsWith("dyn") || [
                "data", "source", "token", "url"
            ]
                .some(k => uti.includes(k))
        )
            ? [[
                uti.split(".").slice(-1)[0],
                uti
            ]]
            : [];


    // ----------------------- JXA -----------------------

    // typesNowInClipboard :: () -> IO (Int, [UTI])
    const typesNowInClipboard = () => {
        // A changeCount Int and a list of UTIs,
        // the former lets us check, before writing to
        // a file that the clipboard contents have
        // not been changed since the type listing.
        const
            pb = $.NSPasteboard.generalPasteboard,
            pbItems = ObjC.unwrap(pb.pasteboardItems);

        return Tuple(
            parseInt(pb.changeCount)
        )(
            0 < pbItems.length
                ? ObjC.deepUnwrap(
                    pbItems[0].types
                )
                : []
        );
    };

    // -------------------- DATE TIME --------------------

    // dateTimeString :: Date -> String
    const dateTimeString = dte =>
        [
            ...second(
                t => zipWith(add)(
                    t.slice(0, 8).split(":")
                        .concat(
                            t.split(".")[1].slice(0, 3)
                        )
                )(["", "", ""]).join("")
                // ["h", "m", "s", "ms"]
            )(
                iso8601Local(dte).split("T")
            )
        ].join("_");


    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
            .toISOString();

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });

    // add (+) :: Num a => a -> a -> a
    const add = a =>
        // Curried addition.
        b => a + b;

    // comparing :: Ord a => (b -> a) -> b -> b -> Ordering
    const comparing = f =>
        // The ordering of f(x) and f(y) as a value
        // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b
                ? -1
                : a > b
                    ? 1
                    : 0;
        };


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];



    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        xy => Tuple(
            xy[0]
        )(
            f(xy[1])
        );


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => [...xs].sort(
            (a, b) => f(a)(b)
        );


    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
            .map(x => x[1]);


    // 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 => Array.from(
            { length: Math.min(xs.length, ys.length) },
            (_, i) => f(xs[i])(ys[i])
        );

    return JSON.stringify(main(), null, 2);
})();
Expand disclosure triangle to view JS source for file saving
ObjC.import("AppKit");

// Chosen NSPasteboard type saved to file
// Rob Trew @2025
// Ver 0.01de

// main :: () -> IO Bool
const main = () => {
    const
        fp = kmvar.local_FilePath,
        pb = $.NSPasteboard.generalPasteboard,
        changeCount = kmvar.local_ChangeCount;

    return either(
        alert(`Not saved – ${kmvar.local_UTI}`)
    )(
        msg => msg
    )(
        bindLR(
            // Has the clipboard changed since the flavours
            // were listed ?
            parseInt(changeCount) === parseInt(pb.changeCount)
                ? (() => {
                    const data = pb.dataForType(kmvar.local_UTI);

                    return data.isNil()
                        ? Left(`Not found in clipboard: ${kmvar.local_UTI}`)
                        : Right(data);
                })()
                : Left(`Clipboard contents changed after macro launch. ${kmvar}`)
        )(
            data => (
                data.writeToFileAtomically(fp, true),
                doesFileExist(fp)
                    ? Right(fp)
                    : Left(`Clip flavor not saved: ${kmvar.local_UTI}`)
            )
        )
    );
};


// ------------------------- 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(
        $(s).stringByStandardizingPath
    );

// ----------------------- 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 = lr =>
    // Bind operator for the Either option type.
    // If lr has a Left value then lr unchanged,
    // otherwise the function mf applied to the
    // Right value in lr.
    mf => "Left" in lr
        ? lr
        : mf(lr.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 => "Left" in e
        ? fl(e.Left)
        : fr(e.Right);


// MAIN ---
return main();
1 Like