Classes/Clipboard.js

const os = require('os');
const { exec, spawn } = require('child_process');
const { basename } = require('path');

ReduceErrors = false; // Silent drops/fixes

class Clipboard {
	/**
	 * Get the contents of the clipboard in Windows (using `PowerShell` and .NET), macOS (`pbpaste`) and FreeBSD/Linus (using `xclip` or `xsel`).
	 * 
	 * The promise returns the clipboard data as an object.
	 * 
	 * Keys are the formats and the values will follow this format: `data:<mimeType>;<encoding>, <data>`
	 * <p>`mimeType`: Mime type of the data</p>
	 * <p>`<encoding>`: How the data is encoded (base64, hex)</p>
	 * <p>`<data>`: Actual format data</p>
	 * 
	 * Here's an example of data that could be returned (the contents is the word `Node.JS` copied from a Google search in Chrome):
	 * Windows:
	 * ```json
	 * {
	 *		"HTML Format": "data:text/html;base64, VgBlAHIAcwBpAG8AbgA6ADAALgA5AA0ACgBTAHQAYQByAHQASABUAE0ATAA6ADAAMAAwADAAMAAwADAAMgA0ADkADQAKAEUAbgBkAEgAVABNAEwAOgAwADAAMAAwADAAMAAwADgAOAA0AA0ACgBTAHQAYQByAHQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAAyADgANQANAAoARQBuAGQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAA4ADQAOAANAAoAUwBvAHUAcgBjAGUAVQBSAEwAOgBoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAuAGMAbwBtAC8AcwBlAGEAcgBjAGgAPwBxAD0AbgBvAGQAZQBqAHMAJgByAGwAegA9ADEAQwAxAEcAQwBFAFUAXwBlAG4AJgBvAHEAPQBuAG8AZABlAGoAcwAmAGEAcQBzAD0AYwBoAHIAbwBtAGUALgAuADYAOQBpADUANwBqADYAOQBpADYAMABqADYAOQBpADYANQBqADYAOQBpADYAMAAuADUAMAA4AGoAMABqADEAJgBzAG8AdQByAGMAZQBpAGQAPQBjAGgAcgBvAG0AZQAmAGkAZQA9AFUAVABGAC0AOAANAAoAPABoAHQAbQBsAD4ADQAKADwAYgBvAGQAeQA+AA0ACgA8ACEALQAtAFMAdABhAHIAdABGAHIAYQBnAG0AZQBuAHQALQAtAD4APABzAHAAYQBuACAAcwB0AHkAbABlAD0AIgBjAG8AbABvAHIAOgAgAHIAZwBiACgANwA3ACwAIAA4ADEALAAgADgANgApADsAIABmAG8AbgB0AC0AZgBhAG0AaQBsAHkAOgAgAFIAbwBiAG8AdABvACwAIABhAHIAaQBhAGwALAAgAHMAYQBuAHMALQBzAGUAcgBpAGYAOwAgAGYAbwBuAHQALQBzAGkAegBlADoAIAAxADQAcAB4ADsAIABmAG8AbgB0AC0AcwB0AHkAbABlADoAIABuAG8AcgBtAGEAbAA7ACAAZgBvAG4AdAAtAHYAYQByAGkAYQBuAHQALQBsAGkAZwBhAHQAdQByAGUAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB2AGEAcgBpAGEAbgB0AC0AYwBhAHAAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB3AGUAaQBnAGgAdAA6ACAANAAwADAAOwAgAGwAZQB0AHQAZQByAC0AcwBwAGEAYwBpAG4AZwA6ACAAbgBvAHIAbQBhAGwAOwAgAG8AcgBwAGgAYQBuAHMAOgAgADIAOwAgAHQAZQB4AHQALQBhAGwAaQBnAG4AOgAgAHMAdABhAHIAdAA7ACAAdABlAHgAdAAtAGkAbgBkAGUAbgB0ADoAIAAwAHAAeAA7ACAAdABlAHgAdAAtAHQAcgBhAG4AcwBmAG8AcgBtADoAIABuAG8AbgBlADsAIAB3AGgAaQB0AGUALQBzAHAAYQBjAGUAOgAgAG4AbwByAG0AYQBsADsAIAB3AGkAZABvAHcAcwA6ACAAMgA7ACAAdwBvAHIAZAAtAHMAcABhAGMAaQBuAGcAOgAgADAAcAB4ADsAIAAtAHcAZQBiAGsAaQB0AC0AdABlAHgAdAAtAHMAdAByAG8AawBlAC0AdwBpAGQAdABoADoAIAAwAHAAeAA7ACAAYgBhAGMAawBnAHIAbwB1AG4AZAAtAGMAbwBsAG8AcgA6ACAAcgBnAGIAKAAyADUANQAsACAAMgA1ADUALAAgADIANQA1ACkAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AdABoAGkAYwBrAG4AZQBzAHMAOgAgAGkAbgBpAHQAaQBhAGwAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AcwB0AHkAbABlADoAIABpAG4AaQB0AGkAYQBsADsAIAB0AGUAeAB0AC0AZABlAGMAbwByAGEAdABpAG8AbgAtAGMAbwBsAG8AcgA6ACAAaQBuAGkAdABpAGEAbAA7ACAAZABpAHMAcABsAGEAeQA6ACAAaQBuAGwAaQBuAGUAIAAhAGkAbQBwAG8AcgB0AGEAbgB0ADsAIABmAGwAbwBhAHQAOgAgAG4AbwBuAGUAOwAiAD4ATgBvAGQAZQAuAGoAcwA8AC8AcwBwAGEAbgA+ADwAIQAtAC0ARQBuAGQARgByAGEAZwBtAGUAbgB0AC0ALQA+AA0ACgA8AC8AYgBvAGQAeQA+AA0ACgA8AC8AaAB0AG0AbAA+AA==",
	 *		"UnicodeText": "data:text/plain;base64, TgBvAGQAZQAuAGoAcwA=",
	 *		"Text": "data:text/plain;base64, TgBvAGQAZQAuAGoAcwA=",
	 *		"Locale": "data:application/octet-stream;hex, 09040000",
	 *		"OEMText": "data:text/cp437;base64, Tm9kZS5qcw==" // Code page 437!
	 * }
	 * ```
	 * 
	 * Linux:
	 * ```json
	 * {
	 *		 "TIMESTAMP": "OTIyMjgwNDYw",
	 *		 "TARGETS":"VElNRVNUQU1QClRBUkdFVFMKVVRGOF9TVFJJTkcKVEVYVAo=",
	 *		 "UTF8_STRING":"Tm9kZS5qcw==",
	 *		 "TEXT":"Tm9kZS5qcw=="
	 * }
	 * ```
	 * 
	 * @param {boolean} [correctWindowsLineEndings=false] - Change line endings to LF only (from CR-LF) on Windows.
	 * @return {Promise<Object>} The contents of the clipboard.
	 */
	static getClipboardData(correctWindowsLineEndings) {
		switch (os.type()) {
			case 'Windows_NT':
				return this.getWindowsClipboardData(correctWindowsLineEndings);
			case 'Linux':
			case 'FreeBSD':
				return this.getUnixClipboardData();
			case 'Darwin':
				// return this.getMacOSClipboardData();
			default:
				throw new Error('Unsupported platform');
		}
	}

	/**
	 * All values are expressed as string value like: `data:<mimeType>;<encoding>, <data>`
	 * <p>`mimeType`: Mime type of the data</p>
	 * <p>`<encoding>`: How the data is encoded (base64, hex)</p>
	 * <p>`<data>`: Actual format data</p>
	 * 
	 * @typedef {object} StandardizedClipboardData 
	 * @property {string} [Image] - Image data, can be PNG, DIB, Bitmap, etc .. Depends on mime type, which is `image/<image type>` (you want `<image type>`).
	 * @property {string} [Text] - Plain text data, either Unicode (UTF8) or ASCII.
	 * @property {string} [RichText] - Rich text data
	 * @property {string} [HTML] - HTML format data
	 */

	/**
	 * Gets the device's clipboard data and fits the data into one of 4 formats. This is to help making the data exchangeable across devices.
	 * 
	 * @param {boolean} [correctWindowsLineEndings=false] - Change line endings to LF only (from CR-LF) on Windows.
	 * @return {Promise<StandardizedClipboardData>}
	 */
	static getStandardizedClipboardData(correctWindowsLineEndings) {
		return new Promise(async (resolve, reject) => {
			try {
				let clipboardData = await this.getClipboardData(correctWindowsLineEndings);
				/** @type {StandardizedClipboardData} */
				let returnData = {}

				let formats = Object.keys(clipboardData);

				let breakUpParts = function(data) {
					let parts = data.split(',');
					if (parts.length != 2)
						return undefined;
					data = parts[1].trimStart();
					if (typeof data !== "string")
						return undefined;


					let pattern = /^data:(.*)\/(.*);(.*)$/;
					let matches = parts[0].match(pattern);
					if (!matches)
						return undefined;

					return {
						mimeType: matches[1].toLowerCase(),
						fileExtension: matches[2].toLowerCase(),
						encoding: matches[3].toLowerCase(),
						data: data
					};
				}

				if (os.type() == "Windows_NT") {
					// Copy image data over
					if (typeof clipboardData.PNG === "string") {
						// Image data
						returnData.Image = clipboardData.PNG;
					} else if (typeof clipboardData.DeviceIndependentBitmap === "string") {
						// CF_DIB
						returnData.Image = clipboardData.DeviceIndependentBitMap;
					} else if (typeof clipboardData.Bitmap === "string") {
						returnData.Image = clipboardData.Bitmap;
					}

					// Copy text over
					if (typeof clipboardData.Unicode === "string") {
						returnData.Text = clipboardData.Unicode;
					} else if (typeof clipboardData.Text === "string") {
						returnData.Text = clipboardData.Text;
					} else if (typeof clipboardData.FileNameW === "string") {
						returnData.Text = clipboardData.FileNameW;
					}

					// Copy RichText over
					if (typeof clipboardData["Rich Text Format"] === "string") {
						returnData.RichText = clipboardData["Rich Text Format"];
					} else if (typeof clipboardData["RTF As Text"] === "string") {
						returnData.RichText = clipboardData["RTF As Text"];
					}

					// Copy HTML data over
					if (typeof clipboardData["HTML Format"] === "string") {
						returnData.HTML = clipboardData["HTML Format"];
					} else if (typeof clipboardData.HTML === "string") {
						returnData.HTML = clipboardData.HTML;
					}


				} else if (os.type() == "Linux" || os.type() == "FreeBSD") {
					// Copy image data over
					if (typeof clipboardData["image/png"] === "string")
						returnData.Image = clipboardData["image/png"];
					else if (typeof clipboardData["image/bmp"] === "string")
						returnData.Image = clipboardData["image/bmp"];
					else if (typeof clipboardData["image/jpeg"] === "string")
						returnData.Image = clipboardData["image/jpeg"];

					// Copy text over
					if (typeof clipboardData.UTF8_STRING === "string")
						returnData.Text = clipboardData.UTF8_STRING;
					else if (typeof clipboardData.TEXT === "string")
						returnData.Text = clipboardData.TEXT;
					else if (typeof clipboardData.STRING === "string")
						returnData.Text = clipboardData.STRING;

					// Copy RichText over
					// ???

					// Copy HTML data over
					if (typeof clipboardData["text/html"] === "string") {
						returnData.HTML = clipboardData["text/html"];
					}
				}

				formats.forEach((format) => {
					try {
						let parts = clipboardData[format].split(',');
						if (parts.length != 2)
							return;
						let data = parts[1].trimStart();
						if (typeof data !== "string")
							return;


						let pattern = /^data:(.*)\/(.*);(.*)$/;
						let matches = parts[0].match(pattern);
						if (!matches)
							return;

						let mimeType = matches[1].toLowerCase();
						let fileExtension = matches[2].toLowerCase();
						let encoding = matches[3].toLowerCase();

						if (os.type() == "Windows_NT" || os.type() == "Linux" || os.type() == "FreeBSD") {
							switch (mimeType) {
								case "image":
									if (fileExtension == "png") {
										returnData.Image = clipboardData[format]; // Go with a PNG over whatever else
									} else if (returnData.Image === undefined) {
										returnData.Image = clipboardData[format]; // Only add image if not already there
									}
									break;

								case "text": {
									if ((fileExtension == "rtf" || fileExtension == "richtext") && returnData.RichText === undefined) {
										returnData.RichText = clipboardData[format];
									} else if (fileExtension == "html" && returnData.HTML === undefined) {
										returnData.HTML = clipboardData[format];
									} else if (returnData.Text !== undefined) {
										returnData.Text = clipboardData[format];
									}
								}
							}


						}
					} catch (e) {}
				});

				// convert to standard HTML
				if (os.type() == "Windows_NT" && typeof returnData.HTML === "string") {
					let data = breakUpParts(returnData.HTML);
					if (data !== undefined) {
						let cfHTML = Buffer.from(data.data, data.encoding).toString('ascii');
						
						let lines = cfHTML.split(/(?:\r\n|\r|\n)/g);
						let htmlData = "";
						let inHTML = false;
						lines.forEach((line) => {
							if (inHTML)
								htmlData += line;
							else if (line.split(':').length < 2) {
								inHTML = true;
								htmlData += line;
							}
						})


						if (htmlData) {
							if (correctWindowsLineEndings)
								htmlData = htmlData.replace(/\r\n/,'\n');

							htmlData = Buffer.from(htmlData, 'ascii').toString("utf8"); // ASCII -> UTF8
							htmlData = Buffer.from(htmlData, "utf8").toString('base64'); // UTF8 -> BASE64

							returnData.HTML = "data:text/html;" + "base64" + ", " + htmlData;
						}
					}
				}

				resolve(returnData);
			} catch (err) {
				reject(err);
			}
		});
	}


	/**
	 * Get the contents of the clipboard in macOS using pbpaste.
	 * 
	 * The promise returns the clipboard data as an object, with the keys referring to the data type and the value being the data value in base64.
	 * 
	 * @return {Promise<Object>} The contents of the clipboard.
	 */
	// static getMacOSClipboardData() {
	// 	// haven't _actually_ checked this!
	// 	return new Promise((resolve, reject) => {
	// 		exec('pbpaste -pboard general -Prefer txt', (error, stdout, stderr) => {
	// 			if (error || stderr) {
	// 				reject(error);
	// 				return;
	// 			}

	// 			let formats = stdout.trim().split('\n');
	// 			let data = {};
	// 			let count = 0;
	// 			formats.forEach((format) => {
	// 				exec(`pbpaste -pboard general -Prefer ${format} | base64`, (err, stdout, stderr) => {
	// 					if (!err) {
	// 						if (stdout)
	// 							data[format] = stdout.trim();
	// 					}

	// 					count++;
	// 					if (count === formats.length) {
	// 						resolve(data);
	// 					}
	// 				});
	// 			});
	// 		});
	// 	});
	// }

	/**
	 * Get the contents of the clipboard in Windows using PowerShell.
	 * 
	 * The promise returns the clipboard data as an object, with the keys referring to the data type and the value being the data value in base64.
	 * 
	 * 	 * Here's an example of data that could be returned (the contents is the word `Node.JS` copied from a Google search in Chrome):
	 * Windows:
	 * ```json
	 * {
	 *		"HTML Format": "VgBlAHIAcwBpAG8AbgA6ADAALgA5AA0ACgBTAHQAYQByAHQASABUAE0ATAA6ADAAMAAwADAAMAAwADAAMgA0ADkADQAKAEUAbgBkAEgAVABNAEwAOgAwADAAMAAwADAAMAAwADgAOAA0AA0ACgBTAHQAYQByAHQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAAyADgANQANAAoARQBuAGQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAA4ADQAOAANAAoAUwBvAHUAcgBjAGUAVQBSAEwAOgBoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAuAGMAbwBtAC8AcwBlAGEAcgBjAGgAPwBxAD0AbgBvAGQAZQBqAHMAJgByAGwAegA9ADEAQwAxAEcAQwBFAFUAXwBlAG4AJgBvAHEAPQBuAG8AZABlAGoAcwAmAGEAcQBzAD0AYwBoAHIAbwBtAGUALgAuADYAOQBpADUANwBqADYAOQBpADYAMABqADYAOQBpADYANQBqADYAOQBpADYAMAAuADUAMAA4AGoAMABqADEAJgBzAG8AdQByAGMAZQBpAGQAPQBjAGgAcgBvAG0AZQAmAGkAZQA9AFUAVABGAC0AOAANAAoAPABoAHQAbQBsAD4ADQAKADwAYgBvAGQAeQA+AA0ACgA8ACEALQAtAFMAdABhAHIAdABGAHIAYQBnAG0AZQBuAHQALQAtAD4APABzAHAAYQBuACAAcwB0AHkAbABlAD0AIgBjAG8AbABvAHIAOgAgAHIAZwBiACgANwA3ACwAIAA4ADEALAAgADgANgApADsAIABmAG8AbgB0AC0AZgBhAG0AaQBsAHkAOgAgAFIAbwBiAG8AdABvACwAIABhAHIAaQBhAGwALAAgAHMAYQBuAHMALQBzAGUAcgBpAGYAOwAgAGYAbwBuAHQALQBzAGkAegBlADoAIAAxADQAcAB4ADsAIABmAG8AbgB0AC0AcwB0AHkAbABlADoAIABuAG8AcgBtAGEAbAA7ACAAZgBvAG4AdAAtAHYAYQByAGkAYQBuAHQALQBsAGkAZwBhAHQAdQByAGUAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB2AGEAcgBpAGEAbgB0AC0AYwBhAHAAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB3AGUAaQBnAGgAdAA6ACAANAAwADAAOwAgAGwAZQB0AHQAZQByAC0AcwBwAGEAYwBpAG4AZwA6ACAAbgBvAHIAbQBhAGwAOwAgAG8AcgBwAGgAYQBuAHMAOgAgADIAOwAgAHQAZQB4AHQALQBhAGwAaQBnAG4AOgAgAHMAdABhAHIAdAA7ACAAdABlAHgAdAAtAGkAbgBkAGUAbgB0ADoAIAAwAHAAeAA7ACAAdABlAHgAdAAtAHQAcgBhAG4AcwBmAG8AcgBtADoAIABuAG8AbgBlADsAIAB3AGgAaQB0AGUALQBzAHAAYQBjAGUAOgAgAG4AbwByAG0AYQBsADsAIAB3AGkAZABvAHcAcwA6ACAAMgA7ACAAdwBvAHIAZAAtAHMAcABhAGMAaQBuAGcAOgAgADAAcAB4ADsAIAAtAHcAZQBiAGsAaQB0AC0AdABlAHgAdAAtAHMAdAByAG8AawBlAC0AdwBpAGQAdABoADoAIAAwAHAAeAA7ACAAYgBhAGMAawBnAHIAbwB1AG4AZAAtAGMAbwBsAG8AcgA6ACAAcgBnAGIAKAAyADUANQAsACAAMgA1ADUALAAgADIANQA1ACkAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AdABoAGkAYwBrAG4AZQBzAHMAOgAgAGkAbgBpAHQAaQBhAGwAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AcwB0AHkAbABlADoAIABpAG4AaQB0AGkAYQBsADsAIAB0AGUAeAB0AC0AZABlAGMAbwByAGEAdABpAG8AbgAtAGMAbwBsAG8AcgA6ACAAaQBuAGkAdABpAGEAbAA7ACAAZABpAHMAcABsAGEAeQA6ACAAaQBuAGwAaQBuAGUAIAAhAGkAbQBwAG8AcgB0AGEAbgB0ADsAIABmAGwAbwBhAHQAOgAgAG4AbwBuAGUAOwAiAD4ATgBvAGQAZQAuAGoAcwA8AC8AcwBwAGEAbgA+ADwAIQAtAC0ARQBuAGQARgByAGEAZwBtAGUAbgB0AC0ALQA+AA0ACgA8AC8AYgBvAGQAeQA+AA0ACgA8AC8AaAB0AG0AbAA+AA==",
	 *		"UnicodeText": "TgBvAGQAZQAuAGoAcwA=",
	 *		"Text": "TgBvAGQAZQAuAGoAcwA=",
	 *		"Locale": "UwB5AHMAdABlAG0ALgBJAE8ALgBNAGUAbQBvAHIAeQBTAHQAcgBlAGEAbQA=",
	 *		"OEMText": "TgBvAGQAZQAuAGoAcwA="
	 * }
	 * ```
	 * 
	 * @param {boolean} [correctLineEndings = false] - Swap CR-LF line endings to LF endings.
	 * @return {Promise<Object>} The contents of the clipboard.
	 */
	static getWindowsClipboardData(correctLineEndings) {
		return new Promise((resolve, reject) => {
			// Define the PowerShell script as a string
			let script = `
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Retrieve a System.Windows.DataObject containing all clipboard data
$dataObject = [System.Windows.Forms.Clipboard]::GetDataObject()

# Create a hashtable to store the base64-encoded data for each format
$output = @{}

# Iterate over each format and retrieve the corresponding clipboard data
foreach ($format in $dataObject.GetFormats()) {
	if ($format.StartsWith("System") -or $format -eq "Embed Source") {
		continue
	} else {
		$data = $dataObject.GetData($format)

		if ($format -eq "PNG" -or $format -eq "BitMap" -or $format -eq "DeviceIndependentBitmap" -or $format -eq "JPG" -or $format -eq "JPEG") {
			if ($dataObject.ContainsImage()) {
				# Retrieve the image data
				$image = $dataObject.GetImage()
				if ($image -ne $null) {
					# Encode the image data in base64
					$stream = New-Object System.IO.MemoryStream

					$pictureFormat = [System.Drawing.Imaging.ImageFormat]::Png
					$mime = "png"
					if ($format -eq "BitMap" -or $format -eq "DeviceIndependentBitmap") {
						$mime = "bmp"
						$pictureFormat = [System.Drawing.Imaging.ImageFormat]::Bmp
					} elseif ($format -eq "JPG" -or $format -eq "JPEG") {
						$mime = "jpeg"
						$pictureFormat = [System.Drawing.Imaging.ImageFormat]::Jpeg
					}

					$image.Save($stream, $pictureFormat)
					$data = [System.Convert]::ToBase64String($stream.ToArray())
					# $data = [BitConverter]::ToString($stream.ToArray()).Replace('-', '')
					$data = "data:image/" + $mime.ToLower() + ";base64, " + $data
				}
			}
		} elseif ($data -ne $null -and $data -ne "") {
			if ($data -is [System.IO.MemoryStream]) {
				$bytes = [System.Byte[]]::CreateInstance([System.Byte],$data.Length)
				$count = $data.Read($bytes, 0, $data.Length)
				# $data = [System.Convert]::ToBase64String($bytes)
				$data = "data:application/octet-stream;hex, " + [BitConverter]::ToString($bytes).Replace('-', '')
			
			} else {
				$encoder = [System.Text.Encoding]::UTF8
				$type = "plain"
				if ($format -eq "RichText" -or $format.StartsWith("Rich Text")) {
					$type = "rtf"
					$format = "RichText"
				} elseif ($format -eq "HTML Format") {
					$type = "html"
					$encoder = [System.Text.Encoding]::ASCII
				} elseif ($format -eq "OEMTEXT") {
					$type = "ibm437"
					$encoder = [System.Text.Encoding]::GetEncoding(437)
				}
				# Encode non-null and non-empty data in base64
				$data = "data:text/$type;base64, " + [System.Convert]::ToBase64String($encoder.GetBytes($data))
			}
		}

		if ($data -ne $null) {
			$output[$format] = $data
		}
	}
}

# Convert the hashtable to JSON format
$jsonOutput = ConvertTo-Json -InputObject $output

# Output the JSON-formatted clipboard data
Write-Output $jsonOutput
Exit
			`;

			// Execute the PowerShell script as a child process
			let powershell = spawn("powershell.exe", ["-NoLogo", "-NoExit", "-noprofile", "-command", script]);

			// Collect the standard output from the PowerShell process
			let stdout = "";
			powershell.stdout.on("data", (data) => {
				stdout += data.toString();
			});
			powershell.stderr.on("data", (data) => {
				reject(data.toString());
			});

			// Handle errors and completion of the PowerShell process
			powershell.on("error", (error) => {
				reject(`PowerShell error: ${error}`);
			});
			powershell.on("exit", (code, signal) => {
				if (code !== 0) {
					reject(`PowerShell process exited with code ${code} and signal ${signal}`);
					return;
				}

				// Parse the JSON-formatted clipboard data
				let clipboardData = JSON.parse(stdout);

				// Resolve the Promise with the clipboard data
				resolve(clipboardData);
			});
		});
	}

	/**
	 * Get the contents of the clipboard in FreeBSD/Linux using xclip or xsel. If neither exist, throw error.
	 * 
	 * xsel only supports standard text.
	 * 
	 * The promise returns the clipboard data as an object, with the keys referring to the data type and the value being the data value in base64.
	 * 
	 * Here's an example of data that could be returned (the contents is the word `Node.JS` copied from a Google search in FireFox):
	 * Linux:
	 * ```json
	 * {
	 *		"text/plain;charset=utf-8": "data:text/plain;base64, Node.js",
	 *		"STRING": "data:text/plain;base64, Tm9kZS5qcw==",
	 *		"TEXT": "data:text/plain;base64, Tm9kZS5qcw==",
	 *		"COMPOUND_TEXT": "data:text/plain;base64, Tm9kZS5qcw==",
	 *		"UTF8_STRING": "data:text/plain;base64, Tm9kZS5qcw==",
	 *		"text/_moz_htmlinfo": "data:text/_moz_htmlinfo;base64, MAAsADAA",
	 *		"text/_moz_htmlcontext":
	 *		"data:text/_moz_htmlcontext;base64, PABoAHQAbQBsACAAaQB0AGUAbQBzAGMAbwBwAGUAPQAiACIAIABpAHQAZQBtAHQAeQBwAGUAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEALgBvAHIAZwAvAFMAZQBhAHIAYwBoAFIAZQBzAHUAbAB0AHMAUABhAGcAZQAiACAAbABhAG4AZwA9ACIAZQBuACIAPgA8AGIAbwBkAHkAIABqAHMAbQBvAGQAZQBsAD0AIgBoAHMAcABEAEQAZgAiACAAYwBsAGEAcwBzAD0AIgBzAHIAcAAiACAAagBzAGMAbwBuAHQAcgBvAGwAbABlAHIAPQAiAEUAbwB4ADMAOQBkACIAIABqAHMAYQBjAHQAaQBvAG4APQAiAHIAYwB1AFEANgBiADoAbgBwAFQAMgBtAGQAOwB4AGoAaABUAEkAZgA6AC4AQwBMAEkARQBOAFQAOwBPADIAdgB5AHMAZQA6AC4AQwBMAEkARQBOAFQAOwBJAFYASwBUAGYAZQA6AC4AQwBMAEkARQBOAFQAOwBFAHoANwBWAE0AYwA6AC4AQwBMAEkARQBOAFQAOwBZAFUAQwA3AEgAZQA6AC4AQwBMAEkARQBOAFQAOwBoAFcAVAA5AEoAYgA6AC4AQwBMAEkARQBOAFQAOwBXAEMAdQBsAFcAZQA6AC4AQwBMAEkARQBOAFQAOwBWAE0AOABiAGcAOgAuAEMATABJAEUATgBUADsAcQBxAGYAMABuADoALgBDAEwASQBFAE4AVAA7AHMAegBqAE8AUgA6AC4AQwBMAEkARQBOAFQAOwBZAGMAZgBKADoALgBDAEwASQBFAE4AVAA7AGsAVwBsAHgAaABjADoALgBDAEwASQBFAE4AVAA7AEoATAA5AFEARABjADoALgBDAEwASQBFAE4AVAA7AGEAZQBCAHIAbgA6AC4AQwBMAEkARQBOAFQAIgAgAGkAZAA9ACIAZwBzAHIAIgAgAHQAbwBwAG0AYQByAGcAaQBuAD0AIgAzACIAIABtAGEAcgBnAGkAbgBoAGUAaQBnAGgAdAA9ACIAMwAiAD4APABkAGkAdgAgAGMAbABhAHMAcwA9ACIAbQBhAGkAbgAiACAAaQBkAD0AIgBtAGEAaQBuACIAPgA8AGQAaQB2ACAAagBzAG0AbwBkAGUAbAA9ACIAIABSAE8AYQBLAHgAZQAiACAAYwBsAGEAcwBzAD0AIgBlADkARQBmAEgAZgAiACAAaQBkAD0AIgBjAG4AdAAiAD4APABkAGkAdgAgAGMAbABhAHMAcwA9ACIARwB5AEEAZQBXAGIAIgAgAGkAZAA9ACIAcgBjAG4AdAAiAD4APABkAGkAdgAgAGMAbABhAHMAcwA9ACIAVABRAGMAMQBpAGQAIAByAGgAcwB0AGMANAAiACAAagBzAGMAbwBuAHQAcgBvAGwAbABlAHIAPQAiAGMAUwBYADkAWABlACIAIABkAGEAdABhAC0AcAB3AHMAPQAiADEAMwAwADAAIgAgAGQAYQB0AGEALQBzAHAAZQA9ACIAdAByAHUAZQAiACAAagBzAGEAYwB0AGkAbwBuAD0AIgByAGMAdQBRADYAYgA6AG4AcABUADIAbQBkACIAIABpAGQAPQAiAHIAaABzACIAIABqAHMAZABhAHQAYQA9ACIATQBkAGUAVgBLAGIAOwBfADsAQQBQAEkANQBsAE0AIgAgAHIAbwBsAGUAPQAiAGMAbwBtAHAAbABlAG0AZQBuAHQAYQByAHkAIgAgAGQAYQB0AGEALQBoAHYAZQBpAGQAPQAiAEMAQgBFAFEAQQBBACIAPgA8AGQAaQB2ACAAagBzAG4AYQBtAGUAPQAiAFQAbABFAEIAcQBkACIAIABqAHMAbQBvAGQAZQBsAD0AIgBIAFgAMgB0AEwAZAAiACAAYwBsAGEAcwBzAD0AIgBrAHAALQB3AGgAbwBsAGUAcABhAGcAZQAgAHMAcwA2AHEAcQBiACAAdQA3AHkAdwA5ACAAegBMAHMAaQBZAGUAIABtAG4AcgAtAGMAIABVAEIAbwB4AEMAYgAgAGsAcAAtAHcAaABvAGwAZQBwAGEAZwBlAC0AbwBzAHIAcAAgAEoAYgAwAFoAaQBmACAARQB5AEIAUgB1AGIAIgAgAGQAYQB0AGEALQBoAHYAZQBpAGQAPQAiAEMARQA4AFEAQQBBACIAIABkAGEAdABhAC0AdgBlAGQAPQAiADIAYQBoAFUASwBFAHcAagBmAHgAXwBfAGQAdABlAG4AOQBBAGgAVgBqAG4ARwBvAEYASABlAG0AMwBEAEkAZwBRADgAZQBzAEMASwBBAEIANgBCAEEAaABQAEUAQQBBACIAPgA8AGQAaQB2AD4APABkAGkAdgAgAGoAcwBjAG8AbgB0AHIAbwBsAGwAZQByAD0AIgBKADQAZwBhADEAYgAiACAAagBzAGQAYQB0AGEAPQAiAHIAagA2AFAAagBmADsAXwA7AEEAUABJADUAbQAwACIAIABqAHMAYQBjAHQAaQBvAG4APQAiAHIAYwB1AFEANgBiADoAbgBwAFQAMgBtAGQAOwBEADIAdwBJAHYAYgA6AFIAOQB6AEkAdABiADsARAB1AEcAYwB6ADoAZgAyADAAegB1AGUAOwB3AGoAZQBFAEYAZQA6AGYAMgAwAHoAdQBlADsATQBwAEgAMwBsAGMAOgBRAFMANwBqAE0AYwA7AG0ATQBmADYAMQBlADoAbwB4AFgAWgAzAGQAOwBpADIAVABqAGMAZAA6AGYAMgAwAHoAdQBlACIAIABjAGwAYQBzAHMAPQAiAG8AcwByAHAALQBiAGwAawAiACAAaQBkAD0AIgBfAFgAYgA4AFgAWgBKAC0AbABIAE8ATwA0AHEAdABzAFAANgBlAC0AeQB3AEEAZwBfADUANQAiAD4APABkAGkAdgA+ADwAZABpAHYAIABjAGwAYQBzAHMAPQAiAEsAbwB0ADcAeAAiAD4APABkAGkAdgAgAGkAZAA9ACIAawBwAC0AdwBwAC0AdABhAGIALQBjAG8AbgB0AC0AbwB2AGUAcgB2AGkAZQB3ACIAIABqAHMAYwBvAG4AdAByAG8AbABsAGUAcgA9ACIAZQB0AEcAUAA0AGMAIgAgAGQAYQB0AGEALQBsAG8AcAByAGkAPQAiADEAIgAgAGQAYQB0AGEALQByAGMAbwB2AD0AIgAxACIAIABqAHMAZABhAHQAYQA9ACIAZwBzAFIATQBHAGIAOwBfADsAQQBQAEkANQBtADQAIgAgAGoAcwBhAGMAdABpAG8AbgA9ACIAcgBjAHUAUQA2AGIAOgBuAHAAVAAyAG0AZAA7AHUAMQA2AGQAWgBlADoAaAAxAGEAcABCAGUAOwBsAGcAcgBBADQAYwA6AEwAWgBSAEgATgBjADsAcwBRAEYAWQBzAGMAOgBKADAAdgBmAFUAZQA7AFYAaQB0AHUAawA6AGkAQgBEADgAZgBjADsAVgBXAEUAdQBIAGYAOgBIAGgANwBXAFEAYgAiACAAZABhAHQAYQAtAGgAdgBlAGkAZAA9ACIAQwBFADgAUQBCAGcAIgAgAGQAYQB0AGEALQB2AGUAZAA9ACIAMgBhAGgAVQBLAEUAdwBqAGYAeABfAF8AZAB0AGUAbgA5AEEAaABWAGoAbgBHAG8ARgBIAGUAbQAzAEQASQBnAFEAeQBkAG8AQgBLAEEAQgA2AEIAQQBoAFAARQBBAFkAIgA+ADwAZABpAHYAIABjAGwAYQBzAHMAPQAiAGEAbwBQAGYATwBjACIAPgA8AGQAaQB2ACAAaQBkAD0AIgBrAHAALQB3AHAALQB0AGEAYgAtAG8AdgBlAHIAdgBpAGUAdwAiACAAZABhAHQAYQAtAGgAdgBlAGkAZAA9ACIAQwBGAFkAUQBBAEEAIgAgAGQAYQB0AGEALQB2AGUAZAA9ACIAMgBhAGgAVQBLAEUAdwBqAGYAeABfAF8AZAB0AGUAbgA5AEEAaABWAGoAbgBHAG8ARgBIAGUAbQAzAEQASQBnAFEAawB0ADQAQgBLAEEAQgA2AEIAQQBoAFcARQBBAEEAIgA+ADwAZABpAHYAIABjAGwAYQBzAHMAPQAiAFQAegBIAEIANgBiACAAYwBMAGoAQQBpAGMAIgAgAGoAcwBjAG8AbgB0AHIAbwBsAGwAZQByAD0AIgBuAFAAYQBRAHUAIgAgAGoAcwBhAGMAdABpAG8AbgA9ACIAcgBjAHUAUQA2AGIAOgBuAHAAVAAyAG0AZAA7AGoAUQBMAEMASwBlADoAVgBpAG0ATwBSAGUAOwAiACAAagBzAGQAYQB0AGEAPQAiAFAAaABvAEgAZAA7AF8AOwBBAFAASQA1AG4AbwAiACAAZABhAHQAYQAtAGgAdgBlAGkAZAA9ACIAQwBJAGMAQgBFAEEAQQAiACAAZABhAHQAYQAtAHYAZQBkAD0AIgAyAGEAaABVAEsARQB3AGoAZgB4AF8AXwBkAHQAZQBuADkAQQBoAFYAagBuAEcAbwBGAEgAZQBtADMARABJAGcAUQAwADQAZwBDAEsAQQBCADYAQgBRAGkASABBAFIAQQBBACIAPgA8AGQAaQB2ACAAagBzAG4AYQBtAGUAPQAiAHgAUQBqAFIATQAiAD4APABkAGkAdgAgAGMAbABhAHMAcwA9ACIAcwBBAFQAUwBIAGUAIgA+ADwAZABpAHYAPgA8AGQAaQB2ACAAYwBsAGEAcwBzAD0AIgBCADAAMwBoADMAZAAgAFYAMQA0AG4ASwBjACAAaQA4AHEAcQA4AGIAIABwAHQAYwBMAEkATwBzAHoAUQBKAHUAXwBfAHcAaABvAGwAZQBwAGEAZwBlAC0AYwBhAHIAZAAgAHcAcAAtAG0AcwAiACAAZABhAHQAYQAtAGgAdgBlAGkAZAA9ACIAQwBJAFEAQgBFAEEAQQAiAD4APABkAGkAdgAgAGMAbABhAHMAcwA9ACIAVQBEAFoAZQBZACAAZgBBAGcAYQBqAGMAIABPAFQARgBhAEEAZgAiAD4APABkAGkAdgAgAGMAbABhAHMAcwA9ACIAeQB4AGoAWgB1AGYAIgA+ADwAZABpAHYAIABjAGwAYQBzAHMAPQAiAHcARABZAHgAaABjACIAIABkAGEAdABhAC0AbQBkAD0AIgA1ADAAIgAgAHMAdAB5AGwAZQA9ACIAYwBsAGUAYQByADoAbgBvAG4AZQAiACAAZABhAHQAYQAtAGgAdgBlAGkAZAA9ACIAQwBGAHcAUQBBAEEAIgAgAGQAYQB0AGEALQB2AGUAZAA9ACIAMgBhAGgAVQBLAEUAdwBqAGYAeABfAF8AZAB0AGUAbgA5AEEAaABWAGoAbgBHAG8ARgBIAGUAbQAzAEQASQBnAFEAawBDAGwANgBCAEEAaABjAEUAQQBBACIAIABsAGEAbgBnAD0AIgBlAG4ALQBVAFMAIgA+ADwAZABpAHYAIABjAGwAYQBzAHMAPQAiAFAAWgBQAFoAbABmACAAaABiADgAUwBBAGMAIgAgAGQAYQB0AGEALQBhAHQAdAByAGkAZAA9ACIAZABlAHMAYwByAGkAcAB0AGkAbwBuACIAIABkAGEAdABhAC0AaAB2AGUAaQBkAD0AIgBDAEYAdwBRAEEAUQAiACAAZABhAHQAYQAtAHYAZQBkAD0AIgAyAGEAaABVAEsARQB3AGoAZgB4AF8AXwBkAHQAZQBuADkAQQBoAFYAagBuAEcAbwBGAEgAZQBtADMARABJAGcAUQB6AGkAQQBvAEEASABvAEUAQwBGAHcAUQBBAFEAIgA+ADwAZABpAHYAIABqAHMAYwBvAG4AdAByAG8AbABsAGUAcgA9ACIARwBDAFMAYgBoAGQAIgAgAGoAcwBhAGMAdABpAG8AbgA9ACIAUwBLAEEAYQBNAGUAOgBjADAAWABVAGIAZQA7AHIAYwB1AFEANgBiADoAbgBwAFQAMgBtAGQAIgA+ADwAZABpAHYAIABqAHMAYwBvAG4AdAByAG8AbABsAGUAcgA9ACIAUQBoAG8AeQBMAGQAIgAgAGoAcwBhAGMAdABpAG8AbgA9ACIAcgBjAHUAUQA2AGIAOgBuAHAAVAAyAG0AZAAiAD4APABkAGkAdgAgAGoAcwBuAGEAbQBlAD0AIgBnADcAVwA3AEUAZAAiACAAagBzAGMAbwBuAHQAcgBvAGwAbABlAHIAPQAiAEcAQwBTAGIAaABkACIAIABjAGwAYQBzAHMAPQAiAGsAbgBvAC0AcgBkAGUAcwBjACIAIABqAHMAYQBjAHQAaQBvAG4APQAiAHMAZQBNADcAUQBlADoAYwAwAFgAVQBiAGUAOwBJAGkAZwBvAGUAZQA6AGMAMABYAFUAYgBlADsAcgBjAHUAUQA2AGIAOgBuAHAAVAAyAG0AZAAiAD4APABzAHAAYQBuAD4APAAvAHMAcABhAG4APgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AZABpAHYAPgA8AC8AYgBvAGQAeQA+ADwALwBoAHQAbQBsAD4A",
	 *		"text/html":
	 *		"data:text/html;base64, PG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiPjxzcGFuPk5vZGUuanM8L3NwYW4+",
	 *		"TARGETS":
	 *		"data:text/plain;base64, VElNRVNUQU1QClRBUkdFVFMKTVVMVElQTEUKU0FWRV9UQVJHRVRTCnRleHQvaHRtbAp0ZXh0L19tb3pfaHRtbGNvbnRleHQKdGV4dC9fbW96X2h0bWxpbmZvClVURjhfU1RSSU5HCkNPTVBPVU5EX1RFWFQKVEVYVApTVFJJTkcKdGV4dC9wbGFpbjtjaGFyc2V0PXV0Zi04CnRleHQvcGxhaW4KdGV4dC94LW1vei11cmwtcHJpdgo=",
	 *		"TIMESTAMP": "data:text/x-timestamp;base64, Mzg0NzI3OAo=",
	 *		"text/plain": "data:text/plain;base64, Tm9kZS5qcw==",
	 *		"text/x-moz-url-priv":
	 *		"data:text/x-moz-url-priv;base64, aAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALgBjAG8AbQAvAHMAZQBhAHIAYwBoAD8AYwBoAGEAbgBuAGUAbAA9AGYAcwAmAGMAbABpAGUAbgB0AD0AdQBiAHUAbgB0AHUAJgBxAD0ATgBvAGQAZQBqAHMA"
	 * }
	 * ```
	 * 
	 * `TARGETS` can be seen as redundant as it's the same as the keys.
	 * 
	 * @return {Promise<Object>} The contents of the clipboard.
	 */
	static getUnixClipboardData() {
		// this is nasty, I know
		return new Promise(async (resolve, reject) => {
			exec('command -v xclip', (err, stdout, stderr) => {
				if (!err && stdout && !stderr) {
					exec('xclip -selection clipboard -o -t TARGETS', (err, stdout, stderr) => {
						if (err) {
							reject(err);
						} else if (stderr) {
							reject(stderr);
						} else {
							let formats = stdout.split('\n').filter(Boolean);
							let data = {};
							let count = 0;

							formats.forEach((format) => {
								if (format !== undefined) {
									exec(`xclip -selection clipboard -o -t ${format} | base64`, (err, stdout, stderr) => {
										if (!err && stdout && !stderr) {
											let mimeType = format.split(';')[0];

											// Fix mime type for things like "TARGETS", "TIMESTAMP", "SAVE_TARGETS" etc
											switch (mimeType) {
												case "STRING":
												case "TEXT":
												case "UTF8_STRING":
												case "COMPOUND_TEXT":
												case "TARGETS":
													mimeType = "text/plain";
													break;

												case "TIMESTAMP":
													mimeType = "text/x-timestamp";
													break;
											}

											if (mimeType.split('/').length != 2) {
												mimeType = "text/" + mimeType.toLowerCase();
											}


											// let base64Data = Buffer.from(stdout).toString('base64');
											let base64Data = stdout.toString().replace(/\n/g,'');
											data[format] = "data:" + mimeType + ";base64, " + base64Data;
										}

										count++;
										if (count === formats.length) {
											resolve(data);
										}
									});
								}
							});
						}
					});
				} else {
					exec('command -v xsel', (err, stdout, stderr) => {
						if (!err && stdout && !stderr) {
							exec('xsel --clipboard -o', (err, stdout, stderr) => {
								if (err)
									reject(err);
								else
									resolve({ TEXT: stdout }); // xsel only does text
							});
						} else {
							reject('Unable to find xclip or xsel.');			
						} // if err
					}); // xsel available?
				} // if err (xclip)
			}); // xclip check
		}); // promise
	}


	//*********************************************
	//	---------------------------------------
	//	Setting
	//	---------------------------------------
	//*********************************************

	/**
	 * Sets the contents of the clipboard in Windows (using `PowerShell` and .NET), macOS (`pbpaste`) and FreeBSD/Linus (using `xclip` or `xsel`).
	 * 
	 * The promise returns a boolean whether or not the data was set successfully.
	 * 
	 * Here's an example of data that could be set (the contents is the word `Node.JS` copied from a Google search in Chrome):
	 * Windows:
	 * ```json
	 * {
	 *		"HTML Format": "VgBlAHIAcwBpAG8AbgA6ADAALgA5AA0ACgBTAHQAYQByAHQASABUAE0ATAA6ADAAMAAwADAAMAAwADAAMgA0ADkADQAKAEUAbgBkAEgAVABNAEwAOgAwADAAMAAwADAAMAAwADgAOAA0AA0ACgBTAHQAYQByAHQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAAyADgANQANAAoARQBuAGQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAA4ADQAOAANAAoAUwBvAHUAcgBjAGUAVQBSAEwAOgBoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAuAGMAbwBtAC8AcwBlAGEAcgBjAGgAPwBxAD0AbgBvAGQAZQBqAHMAJgByAGwAegA9ADEAQwAxAEcAQwBFAFUAXwBlAG4AJgBvAHEAPQBuAG8AZABlAGoAcwAmAGEAcQBzAD0AYwBoAHIAbwBtAGUALgAuADYAOQBpADUANwBqADYAOQBpADYAMABqADYAOQBpADYANQBqADYAOQBpADYAMAAuADUAMAA4AGoAMABqADEAJgBzAG8AdQByAGMAZQBpAGQAPQBjAGgAcgBvAG0AZQAmAGkAZQA9AFUAVABGAC0AOAANAAoAPABoAHQAbQBsAD4ADQAKADwAYgBvAGQAeQA+AA0ACgA8ACEALQAtAFMAdABhAHIAdABGAHIAYQBnAG0AZQBuAHQALQAtAD4APABzAHAAYQBuACAAcwB0AHkAbABlAD0AIgBjAG8AbABvAHIAOgAgAHIAZwBiACgANwA3ACwAIAA4ADEALAAgADgANgApADsAIABmAG8AbgB0AC0AZgBhAG0AaQBsAHkAOgAgAFIAbwBiAG8AdABvACwAIABhAHIAaQBhAGwALAAgAHMAYQBuAHMALQBzAGUAcgBpAGYAOwAgAGYAbwBuAHQALQBzAGkAegBlADoAIAAxADQAcAB4ADsAIABmAG8AbgB0AC0AcwB0AHkAbABlADoAIABuAG8AcgBtAGEAbAA7ACAAZgBvAG4AdAAtAHYAYQByAGkAYQBuAHQALQBsAGkAZwBhAHQAdQByAGUAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB2AGEAcgBpAGEAbgB0AC0AYwBhAHAAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB3AGUAaQBnAGgAdAA6ACAANAAwADAAOwAgAGwAZQB0AHQAZQByAC0AcwBwAGEAYwBpAG4AZwA6ACAAbgBvAHIAbQBhAGwAOwAgAG8AcgBwAGgAYQBuAHMAOgAgADIAOwAgAHQAZQB4AHQALQBhAGwAaQBnAG4AOgAgAHMAdABhAHIAdAA7ACAAdABlAHgAdAAtAGkAbgBkAGUAbgB0ADoAIAAwAHAAeAA7ACAAdABlAHgAdAAtAHQAcgBhAG4AcwBmAG8AcgBtADoAIABuAG8AbgBlADsAIAB3AGgAaQB0AGUALQBzAHAAYQBjAGUAOgAgAG4AbwByAG0AYQBsADsAIAB3AGkAZABvAHcAcwA6ACAAMgA7ACAAdwBvAHIAZAAtAHMAcABhAGMAaQBuAGcAOgAgADAAcAB4ADsAIAAtAHcAZQBiAGsAaQB0AC0AdABlAHgAdAAtAHMAdAByAG8AawBlAC0AdwBpAGQAdABoADoAIAAwAHAAeAA7ACAAYgBhAGMAawBnAHIAbwB1AG4AZAAtAGMAbwBsAG8AcgA6ACAAcgBnAGIAKAAyADUANQAsACAAMgA1ADUALAAgADIANQA1ACkAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AdABoAGkAYwBrAG4AZQBzAHMAOgAgAGkAbgBpAHQAaQBhAGwAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AcwB0AHkAbABlADoAIABpAG4AaQB0AGkAYQBsADsAIAB0AGUAeAB0AC0AZABlAGMAbwByAGEAdABpAG8AbgAtAGMAbwBsAG8AcgA6ACAAaQBuAGkAdABpAGEAbAA7ACAAZABpAHMAcABsAGEAeQA6ACAAaQBuAGwAaQBuAGUAIAAhAGkAbQBwAG8AcgB0AGEAbgB0ADsAIABmAGwAbwBhAHQAOgAgAG4AbwBuAGUAOwAiAD4ATgBvAGQAZQAuAGoAcwA8AC8AcwBwAGEAbgA+ADwAIQAtAC0ARQBuAGQARgByAGEAZwBtAGUAbgB0AC0ALQA+AA0ACgA8AC8AYgBvAGQAeQA+AA0ACgA8AC8AaAB0AG0AbAA+AA==",
	 *		"UnicodeText": "TgBvAGQAZQAuAGoAcwA=",
	 *		"Text": "TgBvAGQAZQAuAGoAcwA=",
	 *		"Locale": "UwB5AHMAdABlAG0ALgBJAE8ALgBNAGUAbQBvAHIAeQBTAHQAcgBlAGEAbQA=",
	 *		"OEMText": "TgBvAGQAZQAuAGoAcwA="
	 * }
	 * ```
	 * 
	 * Linux:
	 * ```json
	 * {
	 *		 "TIMESTAMP": "OTIyMjgwNDYw",
	 *		 "TARGETS":"VElNRVNUQU1QClRBUkdFVFMKVVRGOF9TVFJJTkcKVEVYVAo=",
	 *		 "UTF8_STRING":"Tm9kZS5qcw==",
	 *		 "TEXT":"Tm9kZS5qcw=="
	 * }
	 * ```
	 * 
	 * @param {boolean} [correctWindowsLineEndings=false] - Change line endings to LF only (from CR-LF) on Windows.
	 * @return {Promise<boolean>} - Set successfully
	 */
	static setClipboardData(clipboardData, correctWindowsLineEndings) {
		switch (os.type()) {
			case 'Windows_NT':
				return this.setWindowsClipboardData(clipboardData, correctWindowsLineEndings);
			case 'Linux':
			case 'FreeBSD':
				return this.setUnixClipboardData(clipboardData);
			case 'Darwin':
				// return this.setMacOSClipboardData(clipboardData);
			default:
				throw new Error('Unsupported platform');
		}
	}


	/**
	 * Sets the contents of the clipboard in Windows using PowerShell.
	 * 
	 * The promise returns nothing, only rejects on failure.
	 * 
	 * Data is to be in JSON string format or as an object (key-pairs!) - all of which came from the getWindowsClipboardData() function.
	 * 
	 * Here's an example that sets the clipboard to "Node.JS" with HTML formatting.
	 * 
	 * ```js
	 * setWindowsClipboardData({
	 *		"HTML Format": "VgBlAHIAcwBpAG8AbgA6ADAALgA5AA0ACgBTAHQAYQByAHQASABUAE0ATAA6ADAAMAAwADAAMAAwADAAMgA0ADkADQAKAEUAbgBkAEgAVABNAEwAOgAwADAAMAAwADAAMAAwADgAOAA0AA0ACgBTAHQAYQByAHQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAAyADgANQANAAoARQBuAGQARgByAGEAZwBtAGUAbgB0ADoAMAAwADAAMAAwADAAMAA4ADQAOAANAAoAUwBvAHUAcgBjAGUAVQBSAEwAOgBoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAuAGMAbwBtAC8AcwBlAGEAcgBjAGgAPwBxAD0AbgBvAGQAZQBqAHMAJgByAGwAegA9ADEAQwAxAEcAQwBFAFUAXwBlAG4AJgBvAHEAPQBuAG8AZABlAGoAcwAmAGEAcQBzAD0AYwBoAHIAbwBtAGUALgAuADYAOQBpADUANwBqADYAOQBpADYAMABqADYAOQBpADYANQBqADYAOQBpADYAMAAuADUAMAA4AGoAMABqADEAJgBzAG8AdQByAGMAZQBpAGQAPQBjAGgAcgBvAG0AZQAmAGkAZQA9AFUAVABGAC0AOAANAAoAPABoAHQAbQBsAD4ADQAKADwAYgBvAGQAeQA+AA0ACgA8ACEALQAtAFMAdABhAHIAdABGAHIAYQBnAG0AZQBuAHQALQAtAD4APABzAHAAYQBuACAAcwB0AHkAbABlAD0AIgBjAG8AbABvAHIAOgAgAHIAZwBiACgANwA3ACwAIAA4ADEALAAgADgANgApADsAIABmAG8AbgB0AC0AZgBhAG0AaQBsAHkAOgAgAFIAbwBiAG8AdABvACwAIABhAHIAaQBhAGwALAAgAHMAYQBuAHMALQBzAGUAcgBpAGYAOwAgAGYAbwBuAHQALQBzAGkAegBlADoAIAAxADQAcAB4ADsAIABmAG8AbgB0AC0AcwB0AHkAbABlADoAIABuAG8AcgBtAGEAbAA7ACAAZgBvAG4AdAAtAHYAYQByAGkAYQBuAHQALQBsAGkAZwBhAHQAdQByAGUAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB2AGEAcgBpAGEAbgB0AC0AYwBhAHAAcwA6ACAAbgBvAHIAbQBhAGwAOwAgAGYAbwBuAHQALQB3AGUAaQBnAGgAdAA6ACAANAAwADAAOwAgAGwAZQB0AHQAZQByAC0AcwBwAGEAYwBpAG4AZwA6ACAAbgBvAHIAbQBhAGwAOwAgAG8AcgBwAGgAYQBuAHMAOgAgADIAOwAgAHQAZQB4AHQALQBhAGwAaQBnAG4AOgAgAHMAdABhAHIAdAA7ACAAdABlAHgAdAAtAGkAbgBkAGUAbgB0ADoAIAAwAHAAeAA7ACAAdABlAHgAdAAtAHQAcgBhAG4AcwBmAG8AcgBtADoAIABuAG8AbgBlADsAIAB3AGgAaQB0AGUALQBzAHAAYQBjAGUAOgAgAG4AbwByAG0AYQBsADsAIAB3AGkAZABvAHcAcwA6ACAAMgA7ACAAdwBvAHIAZAAtAHMAcABhAGMAaQBuAGcAOgAgADAAcAB4ADsAIAAtAHcAZQBiAGsAaQB0AC0AdABlAHgAdAAtAHMAdAByAG8AawBlAC0AdwBpAGQAdABoADoAIAAwAHAAeAA7ACAAYgBhAGMAawBnAHIAbwB1AG4AZAAtAGMAbwBsAG8AcgA6ACAAcgBnAGIAKAAyADUANQAsACAAMgA1ADUALAAgADIANQA1ACkAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AdABoAGkAYwBrAG4AZQBzAHMAOgAgAGkAbgBpAHQAaQBhAGwAOwAgAHQAZQB4AHQALQBkAGUAYwBvAHIAYQB0AGkAbwBuAC0AcwB0AHkAbABlADoAIABpAG4AaQB0AGkAYQBsADsAIAB0AGUAeAB0AC0AZABlAGMAbwByAGEAdABpAG8AbgAtAGMAbwBsAG8AcgA6ACAAaQBuAGkAdABpAGEAbAA7ACAAZABpAHMAcABsAGEAeQA6ACAAaQBuAGwAaQBuAGUAIAAhAGkAbQBwAG8AcgB0AGEAbgB0ADsAIABmAGwAbwBhAHQAOgAgAG4AbwBuAGUAOwAiAD4ATgBvAGQAZQAuAGoAcwA8AC8AcwBwAGEAbgA+ADwAIQAtAC0ARQBuAGQARgByAGEAZwBtAGUAbgB0AC0ALQA+AA0ACgA8AC8AYgBvAGQAeQA+AA0ACgA8AC8AaAB0AG0AbAA+AA==",
	 *		"UnicodeText": "TgBvAGQAZQAuAGoAcwA=",
	 *		"Text": "TgBvAGQAZQAuAGoAcwA=",
	 *		"Locale": "UwB5AHMAdABlAG0ALgBJAE8ALgBNAGUAbQBvAHIAeQBTAHQAcgBlAGEAbQA=",
	 *		"OEMText": "TgBvAGQAZQAuAGoAcwA="
	 * });
	 * ```
	 * 
	 * @param {object|string} clipboardData - Clipboard data either in JSON string format or object format.
	 * @param {boolean} [correctLineEndings = false] - Swap LF line endings to CR-LF endings.
	 * @return {Promise<boolean>} No data, true if PowerShell exited. 
	 */
	static setWindowsClipboardData(clipboardData, correctLineEndings) {
		if (typeof clipboardData === "object") {
			try {
				clipboardData = JSON.stringify(clipboardData);
			} catch(e) {
				throw new TypeError("clipboardData expected json string or object, got " + (typeof clipboardData).toString());
			}
		}
		if (typeof clipboardData !== "string")
			throw new TypeError("clipboardData expected object or json string, got " + (typeof clipboardData).toString());



		return new Promise((resolve, reject) => {
			// Define the PowerShell script as a string
			let script = `
$jsonData = Read-Host
$data = ConvertFrom-Json $jsonData

# Add references
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Clear the clipboard
[System.Windows.Forms.Clipboard]::Clear()
$dataObject = New-Object System.Windows.Forms.DataObject

foreach ($type in $data.PSObject.Properties) {
	if ($null -eq $type.Value) {
		continue
	}

	$parts = $type.Value -split ",", 2

	# define the regular expression pattern
	$pattern = "^data:(.*)\/(.*);(.*)$"

	# check if $type.Value matches the pattern
	if ($parts[0] -match $pattern) {
			# extract the MIME type and file extension from the first part
			$mime_type = $matches[1].ToLower()
			$file_extension = $matches[2].ToLower()
			$encoding = $matches[3].ToLower()
			$data = $parts[1].trim()

			if ($encoding -eq "base64") {
				$bytes = [System.Convert]::FromBase64String($data)
			} elseif ($encoding -eq "hex") {
			[byte[]] $bytes = [byte[]] -split ($data -replace '..', '0x$& ')
			}
	} else {
			continue
	}

	if ($file_extension -eq "octet-stream") {
		$dataObject.SetData($type.Name, [System.IO.MemoryStream]::new($bytes))

	} elseif ($mime_type -eq "image") {
		# Convert base64-encoded image data to image
		$imageStream = New-Object System.IO.MemoryStream( , $bytes)
		$image = [System.Drawing.Image]::FromStream($imageStream)
		$dataObject.SetImage($image)

	} elseif ($mime_type -eq "text") {
		$encoder = [System.Text.Encoding]::UTF8
		if ($file_extension -eq "ibm437") {
			$encoder = [System.Text.Encoding]::GetEncoding(437)
		}

		$dataObject.SetData($type.Name, $encoder.GetString($bytes))
	}
}
[System.Windows.Forms.Clipboard]::SetDataObject($dataObject, $true)
Exit
			`;

			// Execute the PowerShell script as a child process
			let powershell = spawn("powershell.exe", ["-NoLogo", "-noprofile", "-command", script]);

			// powershell.stdout.on("data", (data) => {
			// 	console.log(data.toString());
			// });
			// powershell.stderr.on("data", (data) => {
			// 	console.error(data.toString());
			// });

			// Handle errors and completion of the PowerShell process
			powershell.on("error", (error) => {
				reject(`PowerShell error: ${error}`);
			});
			powershell.on("exit", (code, signal) => {
				if (code !== 0) {
					reject(`PowerShell process exited with code ${code} and signal ${signal}`);
					return;
				}
				resolve(true);
			});

			powershell.stdin.write(clipboardData + "\n", (err) => {
				if (err !== undefined) {
					powershell.kill();
					reject(err);
				}
			});
		});
	}


	static setUnixClipboardData(data) {
		// this is nasty, I know
		return new Promise(async (resolve, reject) => {
			exec('command -v xclip', (err, stdout, stderr) => {
				if (!err && stdout && !stderr) {
					let formats = Object.keys(data);
					let count = 0;

					formats.forEach((format) => {
						if (format !== undefined) {
							exec(`echo "${data[format]}" | base64 -d | xclip -selection clipboard -t ${format}`, (err, stdout, stderr) => {
								if (err) {
									reject(err);
								} else if (stderr) {
									reject(stderr);
								}

								count++;
								if (count === formats.length) {
									resolve();
								}
							});
						}
					});
				} else {
					if (data.TEXT !== undefined && data.UTF8_STRING !== undefined)
					exec('command -v xsel', (err, stdout, stderr) => {
						if (!err && stdout && !stderr) {
							let text = data.TEXT !== undefined? data.TEXT : data.UTF8_STRING;
							exec(`echo "${data.TEXT}" | xsel --clipboard`, (err, stdout, stderr) => {
								if (err) {
									reject(err);
								} else if (stderr) {
									reject(stderr);
								}

								resolve();
							});
						} else {
							reject('Unable to find xclip or xsel.');
						}
					});
				}
			});
		});
	}

	static flattenObject(obj, prefix = "", delimiter = "|", arrayDelimiter = "[", result = {}) {
		for (const key in obj) {
			if (obj.hasOwnProperty(key)) {
				const newKey = prefix ? `${prefix}${delimiter}${key}` : key;
				const value = obj[key];

				if (value !== null && value !== undefined) {
					if (typeof value === "object") {
						if (Array.isArray(value)) {
							for (let i = 0; i < value.length; i++) {
								const arrayKey = `${newKey}${arrayDelimiter}${i}`;
								if (typeof value[i] === "object" && !Array.isArray(value[i])) {
									this.flattenObject(value[i], arrayKey, delimiter, arrayDelimiter, result);
								} else {
									result[arrayKey] = value[i];
								}
							}
						} else {
							this.flattenObject(value, newKey, delimiter, arrayDelimiter, result);
						}
					} else {
						result[newKey] = value;
					}
				}
			}
		}
		return result;
	}

	/**
	 * Gets the device's clipboard data and fits the data into one of 4 formats. This is to help making the data exchangeable across devices.
	 * 
	 * Replaces LF line endings to CR-LF on Windows.
	 * @param {StandardizedClipboardData} clipboardData - Clipboard data to set. _This can be in JSON form, follow StandardizedClipboardData scheme._
	 */
	static setStandardizedClipboardData(clipboardData) {
		if (typeof clipboardData === "string") {
			try {
				clipboardData = JSON.parse(clipboardData);
			} catch(e) {
				throw new TypeError("clipboardData expected object got " + (typeof clipboardData).toString());
			}
		}
		if (typeof clipboardData !== "object")
			throw new TypeError("clipboardData expected object got " + (typeof clipboardData).toString());


		/** @type {StandardizedClipboardData} */
		let knownGoodData = {};

		if (typeof clipboardData.Text === "string")
			knownGoodData.Text = clipboardData.Text;
		if (typeof clipboardData.Image === "string")
			knownGoodData.Image = clipboardData.Image;
		if (typeof clipboardData.HTML === "string")
			knownGoodData.HTML = clipboardData.HTML;
		if (typeof clipboardData.RichText === "string")
			knownGoodData.RichText = clipboardData.RichText;

		let breakUpParts = function(data) {
			let parts = data.split(',');
			if (parts.length != 2)
				return undefined;
			data = parts[1].trimStart();
			if (typeof data !== "string")
				return undefined;


			let pattern = /^data:(.*)\/(.*);(.*)$/;
			let matches = parts[0].match(pattern);
			if (!matches)
				return undefined;

			return {
				mimeType: matches[1].toLowerCase(),
				fileExtension: matches[2].toLowerCase(),
				encoding: matches[3].toLowerCase(),
				data: data
			};
		}

		if (knownGoodData.Text != undefined) {
			let data = breakUpParts(knownGoodData.Text);
			if (data !== undefined) {
				if (data.fileExtension != "plain") { // Enforce only plain text
					if (ReduceErrors) {
						delete knownGoodData.Text;
					} else {
						throw new Error("Text data not plain text mime type.");
					}
				}
			} else
				if (ReduceErrors) {
					delete knownGoodData.Text;
				} else {
					throw new Error("Text data invalid structure.");
				}
		}

		if (knownGoodData.HTML != undefined) {
			let data = breakUpParts(knownGoodData.HTML);
			if (data !== undefined) {
				if (data.fileExtension != "html") { // Enforce only plain text
					if (ReduceErrors) {
						delete knownGoodData.HTML;
					} else {
						throw new Error("HTML data invalid mime type.");
					}
				}
			} else {
				if (ReduceErrors) {
					delete knownGoodData.HTML;
				} else {
					throw new Error("HTML data invalid structure.")
				}
			}
		}

		if (knownGoodData.RichText != undefined) {
			let data = breakUpParts(knownGoodData.RichText);
			if (data !== undefined) {
				if (data.fileExtension != "rtf") { // Enforce only plain text
					if (data.fileExtension == "richtext" || data.fileExtension == "richtextformat") { // Try to correct mimeType
						data.fileExtension = "rtf";
						knownGoodData.RichText = `data:${data.mimeType}/${data.fileExtension};${data.encoding}, ${data.data}`
					} else {
						// Invalid mime type!
						if (ReduceErrors) {
							delete knownGoodData.RichText;
						} else {
							throw new Error("RichText data invalid mime type.");
						}
					}
				}
			} else {
				if (ReduceErrors) {
					delete knownGoodData.RichText;
				} else {
					throw new Error("RichText data invalid structure.")
				}
			}
		}

		if (knownGoodData.Image != undefined) {
			let data = breakUpParts(knownGoodData.Image);
			if (data === undefined) {
				if (ReduceErrors) {
					delete knownGoodData.Image;
				} else {
					throw new Error("Image data invalid structure.")
				}
			}
		}

		// Data ready
		let platform = os.type();
		if (platform == 'Windows_NT') {
			return this.setWindowsClipboardData(knownGoodData);
		} else if (platform == "Linux" || platform == "FreeBSD") {
			if (knownGoodData.Image !== undefined) { // Image
				let imageData = breakUpParts(knownGoodData.Image);
				if (imageData.mimeType === "image") {
					knownGoodData[imageData.mimeType + "/" + imageData.fileExtension] = knownGoodData.Image;
					delete knownGoodData.Image;
				}

			} else if (knownGoodData.Text !== undefined) { // Text
				let texdData = breakUpParts(knownGoodData.Text);
				if (textData.mimeType == "text" && textData.fileExtension == "plain") {
					knownGoodData["UTF8_STRING"] = knownGoodData.Text;

					// UTF8 -> ASCII
					let ASCIIText = Buffer.from(textData.data, textData.encoding).toString('ASCII');
					let ASCIIBase64 = Buffer.from(ASCIIText, "ASCII").toString("base64");
					knownGoodData["TEXT"] = "data:" + textData.mimeType + "/" + textData.fileExtension + ";base64, " + ASCIIBase64;
				}

			} else if (knownGoodData.HTML !== undefined) { // HTML
				knownGoodData["text/html"] = knownGoodData.HTML;

			} else if (knownGoodData.RichText !== undefined) { // RichText
				knownGoodData["text/rtf"] = knownGoodData.RichText;
			}

			return this.setUnixClipboardData(knownGoodData);
		}
	}
}

module.exports = Clipboard;