{"version":3,"file":"rich-clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/rich-clipboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC3B,2EAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,wEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;CACb;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoBnD;AA+DD;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAiBpF","sourcesContent":["/**\n * Putting two flavours of the same thing on the clipboard at once.\n *\n * A clipboard holds one *content* in several representations, and the pasting\n * application picks the richest it understands. That is the whole mechanism\n * behind a paste into Word keeping its headings while the same paste into a\n * terminal arrives as text. `copyToClipboard` writes one flavour — plain text —\n * so a transcript pasted into Word arrived as a wall of markdown with the\n * asterisks still in it.\n *\n * So: markdown on `text/plain`, HTML on the platform's rich flavour, both from\n * one call. Nothing about the copy changes for a plain-text target.\n *\n * ## What each platform can carry\n *\n * - **macOS** takes both onto the general pasteboard in one clearing, so a\n *   paste into Word is rich and a paste into a terminal is the markdown.\n * - **Windows** takes both through a `DataObject`, with the HTML wrapped in the\n *   CF_HTML envelope the format requires (and PowerShell run `-STA`, because\n *   the clipboard APIs are apartment-threaded).\n * - **Linux** cannot: X11 and Wayland clipboards are owned by one process\n *   advertising one set of targets, and `xclip`/`wl-copy` advertise the single\n *   type they were given. Offering HTML there would mean *replacing* the plain\n *   text with HTML source for every other paste target, which is a worse\n *   clipboard than the one we started with. So Linux gets the markdown, and the\n *   caller is told which flavour landed rather than left to guess.\n *\n * Every path falls back to `copyToClipboard`: a rich copy that fails is still a\n * copy, and the user finds out from the status line what they got.\n */\n\nimport { spawn } from \"child_process\";\nimport { mkdtempSync, rmSync, writeFileSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\nimport { copyToClipboard } from \"./clipboard.js\";\n\n/** What actually reached the clipboard. */\nexport type CopyFlavour = \"rich\" | \"text\";\n\nexport interface RichPayload {\n\t/** The `text/plain` flavour — markdown, for anything that takes text. */\n\ttext: string;\n\t/** The rich flavour — an HTML *fragment*, no `<html>` or `<body>`. */\n\thtml: string;\n}\n\n/**\n * Past this, the rich flavour is dropped rather than handed to a shell.\n *\n * Not a correctness bound — the payloads go through files, not arguments — but\n * a sanity one: a megabyte of HTML on the clipboard stalls the applications\n * that try to render a preview of it, and a session that large is an export,\n * not a paste.\n */\nconst MAX_RICH_BYTES = 2_000_000;\n\n/** Run a command to completion, quietly; resolves false on any failure. */\nfunction run(command: string, args: string[]): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\ttry {\n\t\t\tconst child = spawn(command, args, { stdio: [\"ignore\", \"ignore\", \"ignore\"] });\n\t\t\tchild.on(\"error\", () => resolve(false));\n\t\t\tchild.on(\"close\", (code) => resolve(code === 0));\n\t\t} catch {\n\t\t\tresolve(false);\n\t\t}\n\t});\n}\n\n/**\n * The CF_HTML envelope Windows requires.\n *\n * The header carries byte offsets into the string that contains it, which makes\n * it self-referential: the offsets are only correct once they are the width\n * they will be when written. Fixed-width zero-padded fields are how the format\n * solves that, and why these are padded to ten digits rather than printed\n * plainly.\n *\n * Exported for its own test: the arithmetic is invisible until a paste lands in\n * Word with its first tag missing, which is a long way from here.\n */\nexport function wrapCfHtml(fragment: string): string {\n\tconst header = \"Version:0.9\\r\\nStartHTML:%%%1\\r\\nEndHTML:%%%2\\r\\nStartFragment:%%%3\\r\\nEndFragment:%%%4\\r\\n\";\n\tconst open = \"<html><body>\\r\\n<!--StartFragment-->\";\n\tconst close = \"<!--EndFragment-->\\r\\n</body></html>\";\n\tconst pad = (value: number): string => value.toString().padStart(10, \"0\");\n\tconst headerLength = Buffer.byteLength(header.replace(/%%%\\d/g, \"0000000000\"), \"utf8\");\n\tconst startHtml = headerLength;\n\tconst startFragment = startHtml + Buffer.byteLength(open, \"utf8\");\n\tconst endFragment = startFragment + Buffer.byteLength(fragment, \"utf8\");\n\tconst endHtml = endFragment + Buffer.byteLength(close, \"utf8\");\n\treturn (\n\t\theader\n\t\t\t.replace(\"%%%1\", pad(startHtml))\n\t\t\t.replace(\"%%%2\", pad(endHtml))\n\t\t\t.replace(\"%%%3\", pad(startFragment))\n\t\t\t.replace(\"%%%4\", pad(endFragment)) +\n\t\topen +\n\t\tfragment +\n\t\tclose\n\t);\n}\n\n/**\n * macOS: both flavours onto the general pasteboard, in one clearing.\n *\n * Through JXA (`osascript -l JavaScript`) rather than AppleScript, and the\n * reason is encoding. AppleScript's name for a raw pasteboard type is\n * `«class HTML»`, so the script itself has to carry non-ASCII, and `osascript`\n * decodes a plain-text script file by the current locale — on a machine whose\n * locale is not UTF-8 the chevrons arrive mangled and the script fails to\n * compile. This script is pure ASCII whatever the payload is, because both\n * payloads are *read from files* rather than quoted into it: a transcript is\n * exactly the kind of text that contains quotes, backslashes and newlines, and\n * escaping it into a script literal is a bug waiting for the first code block.\n *\n * `clearContents` before the writes is what makes the two a single item on the\n * pasteboard, which is what lets one paste choose between them.\n */\nasync function copyRichDarwin(payload: RichPayload, dir: string): Promise<boolean> {\n\tconst htmlPath = join(dir, \"clip.html\");\n\tconst textPath = join(dir, \"clip.txt\");\n\tconst scriptPath = join(dir, \"clip.js\");\n\twriteFileSync(htmlPath, payload.html, \"utf8\");\n\twriteFileSync(textPath, payload.text, \"utf8\");\n\tconst read = (path: string): string =>\n\t\t`$.NSString.stringWithContentsOfFileEncodingError(${JSON.stringify(path)}, $.NSUTF8StringEncoding, null)`;\n\tconst script = [\n\t\t\"ObjC.import('AppKit');\",\n\t\t\"var pb = $.NSPasteboard.generalPasteboard;\",\n\t\t\"pb.clearContents;\",\n\t\t`pb.setStringForType(${read(htmlPath)}, $.NSPasteboardTypeHTML);`,\n\t\t`pb.setStringForType(${read(textPath)}, $.NSPasteboardTypeString);`,\n\t].join(\"\\n\");\n\twriteFileSync(scriptPath, script, \"utf8\");\n\treturn await run(\"osascript\", [\"-l\", \"JavaScript\", scriptPath]);\n}\n\n/**\n * Windows: a `DataObject` carrying CF_HTML and Unicode text.\n *\n * `-STA` is not optional. The clipboard is a single-threaded-apartment API and\n * PowerShell's default MTA host silently fails the call, which is the kind of\n * failure that reads as \"copy did nothing\" rather than as an error.\n */\nasync function copyRichWin32(payload: RichPayload, dir: string): Promise<boolean> {\n\tconst htmlPath = join(dir, \"clip.html\");\n\tconst textPath = join(dir, \"clip.txt\");\n\tconst scriptPath = join(dir, \"clip.ps1\");\n\twriteFileSync(htmlPath, wrapCfHtml(payload.html), \"utf8\");\n\twriteFileSync(textPath, payload.text, \"utf8\");\n\tconst script = [\n\t\t\"Add-Type -AssemblyName System.Windows.Forms\",\n\t\t`$html = [System.IO.File]::ReadAllText(${JSON.stringify(htmlPath)})`,\n\t\t`$text = [System.IO.File]::ReadAllText(${JSON.stringify(textPath)})`,\n\t\t\"$data = New-Object System.Windows.Forms.DataObject\",\n\t\t\"$data.SetData([System.Windows.Forms.DataFormats]::Html, $html)\",\n\t\t\"$data.SetData([System.Windows.Forms.DataFormats]::UnicodeText, $text)\",\n\t\t\"[System.Windows.Forms.Clipboard]::SetDataObject($data, $true)\",\n\t].join(\"\\n\");\n\twriteFileSync(scriptPath, script, \"utf8\");\n\treturn await run(\"powershell\", [\"-NoProfile\", \"-STA\", \"-ExecutionPolicy\", \"Bypass\", \"-File\", scriptPath]);\n}\n\n/**\n * Copy `payload`, richly where the platform allows it.\n *\n * @returns which flavour reached the clipboard: `\"rich\"` when the receiving\n *   application can paste structure, `\"text\"` when it will get the markdown.\n *   The caller reports this, because \"copied\" meaning two different things\n *   depending on the machine is how a feature gets reported as broken.\n */\nexport async function copyRichToClipboard(payload: RichPayload): Promise<CopyFlavour> {\n\tconst oversized = Buffer.byteLength(payload.html, \"utf8\") > MAX_RICH_BYTES;\n\tif (!oversized && (process.platform === \"darwin\" || process.platform === \"win32\")) {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"hoocode-clip-\"));\n\t\ttry {\n\t\t\tconst copied =\n\t\t\t\tprocess.platform === \"darwin\" ? await copyRichDarwin(payload, dir) : await copyRichWin32(payload, dir);\n\t\t\tif (copied) return \"rich\";\n\t\t} catch {\n\t\t\t// Fall through to the plain copy: a rich copy that failed is not a\n\t\t\t// reason for the user to lose the text as well.\n\t\t} finally {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t}\n\tawait copyToClipboard(payload.text);\n\treturn \"text\";\n}\n"]}