DocOck, your steer was the right one and it works. Nr.5, your distinction was exactly the point I had muddled: we needed JXA at application level, not Execute JavaScript in Front Window. Nige_S, your two questions turned out to be the most useful things in the thread and I have answered both to the best of my ability below.
The design. The Custom HTML Prompt builds three versions of the report, stacks them into one string separated by a marker, and passes the lot to a helper macro as the trigger parameter. The macro is four actions: set a variable from %TriggerValue%, write it to a temporary file as UTF-8, run a shell script, delete the file. The macro, which is well commented, is below:
Keyboard_Copy Prompt Output Report (Helper Macro).kmmacros (28.6 KB)
Macro Image
The script places all three flavours on the clipboard in a single operation:
Shell Script
osascript -l JavaScript <<'JXA'
ObjC.import('AppKit');
var P = '/tmp/keyboard maestro_prompt output_copy.txt';
var fm = $.NSFileManager.defaultManager;
if (!fm.fileExistsAtPath(P)) throw new Error('hand-off file missing');
var s = $.NSString.stringWithContentsOfFileEncodingError(P, $.NSUTF8StringEncoding, null).js;
var parts = s.split('\n@@KMCLIP@@\n');
if (parts.length !== 3) throw new Error('expected 3 parts, got ' + parts.length);
var rtf = parts[0], html = parts[1], text = parts[2];
var d = $.NSString.alloc.initWithUTF8String(rtf).dataUsingEncoding($.NSUTF8StringEncoding);
var att = $.NSAttributedString.alloc.initWithRTFDocumentAttributes(d, null);
if (!att || att.isNil()) throw new Error('RTF will not parse; clipboard left untouched');
var pb = $.NSPasteboard.generalPasteboard;
pb.clearContents;
pb.setStringForType($(rtf), $('public.rtf')); // TextEdit reads this
pb.setStringForType($(html), $('public.html')); // Word and Excel read this
pb.setStringForType($(text), $('public.utf8-plain-text')); // everything else
JXA
That RTF check before clearContents earns its place: if the incoming RTF is ever malformed the script stops and the user's existing clipboard survives, rather than being wiped and replaced with rubbish.
Nige_S, on Named Clipboards. I tried but I curl not get to work, and I checked properly rather than guessing. Here is Keyboard Maestro's complete list of clipboard actions:
ACopyClipboard ARemoveClipboardFlavors ASetClipboardToText
ASetClipboardToImage ASetClipboardToFileReference ASetClipboardToPastClipboard
There is no add, no append, no set-flavour. Every Set replaces the entire clipboard and Copy Clipboard duplicates one wholesale. So a Named Clipboard will faithfully preserve every flavour of something it captures, but nothing anywhere lets you build one up in stages: the moment you write the HTML you have destroyed the RTF, Named Clipboard or System.
Nige_S, on whether RTF alone would do. Very nearly. RTF alone serves TextEdit and Word correctly. Excel is the holdout: it needs the HTML, so both have to be present together. That is the entire reason for the exercise.
Five things I learnt (with Claude Code's help), in case they save someone else a day.
1. A Custom HTML Prompt cannot place RTF, and it fails silently. I tried text/rtf, public.rtf, application/rtf, text/richtext and com.apple.flat-rtfd. Every one is accepted with no error and none arrives. WebKit diverts the data to com.apple.WebKit.custom-pasteboard-data, which only web pages can read. There is nothing to catch, so it is very easy to believe this is working when it is not.
2. %TriggerValue% carries RTF perfectly. I worried about the backslashes, so I tested it with backslashes, escaped backslash pairs, braces, newlines, a dagger, and the literal text %TriggerValue% and %Variable%Local_Foo% embedded inside the payload. All 263 characters arrived byte for byte identical, and Keyboard Maestro did not attempt to expand those embedded tokens a second time.
3. Read File to System Clipboard is excellent, with one limit. Pointed at a .rtf file it delivers genuine rich text and preserves exact column widths through the round trip; I measured 390pt / 10.5pt / 67.5pt in and the same out. But it always converts to styled text. I fed it a .webarchive containing an HTML table hoping to reach the HTML flavour, and it placed styled text instead, so pasting into Excel put every table cell on its own row down one column. If you only need RTF it is the simplest possible answer and needs no scripting at all.
4. Three RTF traps, all of which cost me a round of testing.
-
\cellx alone is not enough for column widths. A row carrying only \cellx parses as widths of zero and the reader spreads the columns evenly. You need \clwWidth with \clftsWidth3 on every cell.
-
"No border" is \brdrnil, not \brdrnone. \brdrnone is not an RTF keyword, so a reader ignores it silently and falls back to drawing its own full grid. The tables printed with a grid because the instruction was never understood, not because it was disobeyed.
-
Avoid horizontally merged cells. I used \clmgf and \clmrg for full-width section headings, the natural equivalent of colspan. TextEdit does not lay a merged row out against the table's own columns; it redistributes them evenly, so the strip came out narrower than the table and its label wrapped. Emitting one cell per column, shading them all and putting the text in the first, looks identical and survives layout.
5. A control word runs until a non-letter. \intbl followed directly by text swallows the first word, so \intblTest Personal Checking loses the "Test". Close every control word with a space.
On textutil, gently. It is the same converter TextEdit uses on pasted HTML, and it discards every column width. I tried pixel widths, percentages, point widths, widths on <col>, widths on <td>, fixed layout and automatic layout: seven forms, all discarded. Interestingly colspan does survive as a genuine cell merge, so a uniform grid plus colspan is the one way to express proportions through it. For our case we bypassed it and generated the RTF ourselves.
The result is that one Copy button now serves all three applications: Word, Excel and TextEdit each take the version that suits them, with shading, alignment, rules, bold, footnotes and Excel's number formats all intact. TextEdit in particular has never worked properly in any previous attempt at this, until now.
One caveat worth stating plainly, since it cost me most of a day. Excel will not take column widths from a paste, from any source. Not from our HTML, not from HTML that Windows itself built as CF_HTML, and not from XML Spreadsheet, Excel's own native clipboard format, created inside Windows so nothing was lost in transit. In that last case Excel took the data and applied the number formats and still ignored the widths, and did not even offer Keep Source Column Widths. The same HTML opened as a file sets the widths perfectly, so the markup is not at fault: pasting drops content into a sheet that already has its own columns, and Microsoft documents column width as a separate opt-in paste option rather than part of an ordinary paste. So Excel arrives with correct content, formatting and number formats, and default column widths. A double-click on the column edge fixes it, and nothing I send can (at least that I thought of trying). If you have a solution, please let me know.
Thanks again, all three of you.