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