When pasting from the clipboard with multiple items selected, how can I insert a separator (tab or paragraph mark, or space) between the items? When I paste now, all the items are run together.
Thanks.
Good question – I can't immediately see the proper route to that either ...
In the meanwhile, for text clipboards at least, an interim script, which throws up a menu of the named clipboards, and allows you to specify a delimiter in a variable at the top of the macro.
Paste chosen (KM named) clipboards with delimiter.kmmacros (31.4 KB)
JS Source:
(() => {
'use strict';
// Rob Trew 2017-12-23
// GENERIC FUNCTIONS -----------------------------------------------------
// append (++) :: [a] -> [a] -> [a]
const append = (xs, ys) => xs.concat(ys);
// catMaybes :: [Maybe a] -> [a]
const catMaybes = ls =>
concatMap(m => m.nothing ? [] : m.just, ls);
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.length > 0 ? [].concat.apply([], xs.map(f)) : [];
// Handles two or more arguments
// curry :: ((a, b) -> c) -> a -> b -> c
const curry = (f, ...args) => {
const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
function () {
return go(xs.concat(Array.from(arguments)));
};
return go([].slice.call(args));
};
// elemIndex :: Eq a => a -> [a] -> Maybe Int
const elemIndex = (x, xs) => {
const i = xs.indexOf(x);
return {
nothing: i === -1,
just: i
};
};
// enumFromTo :: Int -> Int -> [Int]
const enumFromTo = (m, n) =>
Array.from({
length: Math.floor(n - m) + 1
}, (_, i) => m + i);
// fst :: (a, b) -> a
const fst = pair => pair.length === 2 ? pair[0] : undefined;
// headMay :: [a] -> Maybe a
const headMay = xs =>
xs.length > 0 ? just(xs[0]) : nothing();
// justifyRight :: Int -> Char -> String -> String
const justifyRight = (n, cFiller, strText) =>
n > strText.length ? (
(cFiller.repeat(n) + strText)
.slice(-n)
) : strText;
// just :: a -> Just a
const just = x => ({
nothing: false,
just: x
});
// log :: a -> IO ()
const log = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// nothing :: () -> Nothing
const nothing = (optionalMsg) => ({
nothing: true,
msg: optionalMsg
});
// readFile :: FilePath -> IO String
const readFile = strPath => {
var error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
$(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return typeof error.code !== 'string' ? (
str
) : 'Could not read ' + strPath;
};
// take :: Int -> [a] -> [a]
const take = (n, xs) => xs.slice(0, n);
// takeBaseName :: FilePath -> String
const takeBaseName = strPath =>
strPath !== '' ? (
strPath[strPath.length - 1] !== '/' ? (
strPath.split('/')
.slice(-1)[0].split('.')[0]
) : ''
) : '';
// takeExtension :: FilePath -> String
const takeExtension = strPath => {
const
xs = strPath.split('.'),
lng = xs.length;
return lng > 1 ? (
'.' + xs[lng - 1]
) : '';
};
// File name template -> temporary path
// (Random digit sequence inserted between template base and extension)
// tempFilePath :: String -> IO FilePath
const tempFilePath = template =>
ObjC.unwrap($.NSTemporaryDirectory()) +
takeBaseName(template) + Math.random()
.toString()
.substring(3) + takeExtension(template);
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// unzip :: [(a,b)] -> ([a],[b])
const unzip = xys =>
xys.reduceRight(([xs, ys], [x, y]) => [
[x].concat(xs), [y].concat(ys)
], [
[],
[]
]);
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = (f, xs, ys) =>
Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => f(xs[i], ys[i], i));
// JXA --------------------------------------------------------------------
// menuChoices :: (String a | Num a) => [a] -> [b] -> [a] -> String ->
// String -> String -> String -> Bool -> Bool -> [b]
const menuChoices = (
lstNames, lstValues, lstDefault, strTitle, strPrompt,
strOK, strEsc, blnManyOK, blnEmptyOK
) => {
const intMenu = lstNames.length;
return intMenu > 0 ? (() => {
const
se = Application('System Events'),
sa = (se.includeStandardAdditions = true, se),
procsFront = se.applicationProcesses.where({
frontmost: true
}),
mbFront = procsFront.length > 0 ? (
just(procsFront.at(0)
.bundleIdentifier())
) : nothing('No front application'),
intChars = intMenu.toString()
.length,
paddedIndex = (n, s) =>
justifyRight(intChars, '0',
n.toString()) + '\t' + s,
lstMenu = zipWith(
paddedIndex, enumFromTo(1, intMenu), lstNames
),
result = (
se.activate(),
se.chooseFromList(
lstMenu, {
withTitle: strTitle || '',
withPrompt: strPrompt || 'Choose:',
defaultItems: Array.isArray(lstDefault) ?
concatMap(
k => {
const mb = elemIndex(k, lstNames);
return mb.nothing ? [] : (
[lstMenu[mb.just]]
);
},
lstDefault
)
.slice(
0, blnManyOK ? undefined : 1
) : blnEmptyOK ? [] : lstMenu[0],
okButtonName: strOK || 'OK',
cancelButtonName: strEsc || 'Cancel',
multipleSelectionsAllowed: blnManyOK === false ? (
false
) : true,
emptySelectionAllowed: blnEmptyOK || false
}));
if (!mbFront.nothing) {
Application(mbFront.just)
.activate();
delay(0.2);
}
return (
typeof result !== 'boolean' ? (
result.map(
s => (lstValues || lstNames)[
parseInt(s.split('\t')[0], 10) - 1
]
)
) : []
);
})() : [];
};
// KM VARIABLES ----------------------------------------------------------
// Testing whether this code is executing inside a KM macro,
// and if so, supporting instance-sensitive variable resolutions,
// for example for Local or Instance variables.
// kmInstanceMay :: -> Maybe KM Instance
const kmInstanceMay = () => {
const oInstance = ObjC.unwrap(
$.NSProcessInfo.processInfo.environment
.objectForKey('KMINSTANCE')
);
return Boolean(oInstance) ? (
just(oInstance)
) : nothing('This code is not running in a KM Macro');
};
// kmValueMay :: Maybe KM Instance -> String -> Maybe String
const kmValueMay = (mbInstance, k) => {
const
kme = Application("Keyboard Maestro Engine"),
v = mbInstance.nothing ? (
kme.getvariable(k)
) : kme.getvariable(k, {
instance: mbInstance.just
});
return v === '' ? (
nothing('Empty string returned for ' + k)
) : just(v);
};
// At least one KMVAR key -> Maybe value of first key to return one
// kmValueOrAltMay :: [String] -> Maybe String
const kmValueOrAltMay = ks =>
headMay(catMaybes(
map(curry(kmValueMay)(kmInstanceMay()), ks)
));
// KM PERFORM ACTIONS ----------------------------------------------------
// jsoDoScript :: Object (Dict | Array) -> IO ()
const jsoDoScript = jso => {
const strPath = tempFilePath('tmp.plist');
return (
Application('Keyboard Maestro Engine')
.doScript((
$(Array.isArray(jso) ? jso : [jso])
.writeToFileAtomically(
$(strPath)
.stringByStandardizingPath,
true
),
readFile(strPath)
)),
true
);
};
// MAIN -------------------------------------------------------------------
const
mbDelim = kmValueOrAltMay(['InstanceDelim', 'testDelim']),
strDelim = mbDelim.nothing ? '\n\n' : mbDelim.just,
pairs = map(x => [x.Name, x.UID],
ObjC.deepUnwrap($.NSArray.arrayWithContentsOfFile(
$('~/Library/Application Support/' +
'Keyboard Maestro/Keyboard Maestro Clipboards.plist')
.stringByStandardizingPath
))),
choices = menuChoices(
map(fst, pairs), pairs, undefined,
'KM Named clipboards',
'⌘-Click to select clipboards:'
),
actions = concatMap(
([strName, uid]) => [{
"Delete": false,
//"Second": "100",
"First": "0",
"Destination": "Variable",
"MacroActionType": "Substring",
"NamedClipboardName": uid,
"RedundandDisplayName": strName,
"Source": "NamedClipboard",
"StringRangeType": "From",
"DestinationVariable": "InstanceClip"
},
{
"Variable": "InstanceAccum",
"MacroActionType": "SetVariableToText",
"Text": "%Variable%InstanceAccum%%Variable%InstanceClip%" +
strDelim
}
],
choices
);
return actions.length > 0 ? (
jsoDoScript(append(actions, [{
"MacroActionType": "SetClipboardToText",
"Text": "%Variable%InstanceAccum%",
"JustDisplay": false
},
{
"Action": "Paste",
"IsDisclosed": false,
"MacroActionType": "CutCopyPaste",
"TimeOutAbortsMacro": true
}
])),
'Pasted with delimiter: ' + strDelim + '\n' + unlines(
map(fst, choices)
)
) : 'No clipboards chosen';
})();
This is a clever solution. I had hoped for something simpler, but this should work for my purposes.
Thanks.
There is no solution to this because there is no way to specify what the separator might be. I can’t imagine any useful UI for that I’m afraid.
Thanks for the reply
Can’t you add a popup (dropdown) menu to choose a separator?
Peter, perhaps as a minimum you could offer to separate each clipboard item with a LF (or maybe a CR). IMO, it is a rare use case to concatenate all items without any separator.
So, perhaps using an OPT-RETURN would paste the selected items with some separator.
While a simple LF would be better than nothing, you could offer the following choices for separator:
- LF
- CR
- Rich text of HTML
<HR>
(with a LF before and after) -
---
(with LF before/after. This would translate as a<HR>
in Markdown)
Thanks for considering this suggestion.