Download data from a Custom HTML Prompt?

Hello,

I've been struggling with this all day, and I've admitted defeat and saved the data to a KM Variable in the meantime until I can solve this problem.

In short - I can't find a way to download data from a Custom HTML prompt that doesn't involve using a KM variable as an intermediary. I've made a table that presents data to my from my database, and I'd rather not use a KM variable incase I need to save so much data that it might overpopulate the KM environment.

My most involved attempt revolved around trying to create a blob file from the CSV data to download, either with a URL or URI. In KM it presents an error from Mac OS's dialogue that it can't find an application like so:

"There is no application set to open the URL blob:null/26580a6e-86c7-439c-86b7-21637dfa55fb."

I'm generating this code from an 'execute shell script' that populates the HTML with data from a CSV file. Below is the javascript from the HTML that I've been trying to use to create a download:

		// Define the downloadBlobAsFile function
		const downloadBlobAsFile = (function closure_shell() {
		    const a = document.createElement(\"a\");
		    return function downloadBlobAsFile(blob, filename) {
		        const object_URL = URL.createObjectURL(blob);
		        a.href = object_URL;
		        a.download = filename;
		        a.click();
		        URL.revokeObjectURL(object_URL);
		    };
		})();
        
        // Function to save the spreadhseet data as a CSV
		function saveChanges() {
		    var table = document.getElementById('csvTable');
		    var rows = table.getElementsByTagName('tr');
		    var csvContent = [];
		    for (var i = 1; i < rows.length; i++) {
		        if (rows[i].style.display !== 'none') {
		            var cells = rows[i].getElementsByTagName('td');
		            var rowData = [];
		            for (var j = 1; j < cells.length; j++) {
		                rowData.push(cells[j].textContent.replace(/,/g, ''));
		            }
		            while (rowData.length > 0 && rowData[rowData.length - 1] === '') {
		                rowData.pop();
		            }
		            csvContent.push(rowData.join(','));
		        }
		    }
		
		    var csvString = csvContent.join('\n');
		    
		    // Create a Blob with the CSV content
		    var blob = new Blob([csvString], {type: \"text/csv;charset=utf-8\"});
		    
		    // Use the downloadBlobAsFile function to trigger the download
		    downloadBlobAsFile(blob, \"IncomingCSV.csv\");
		    
		    // Show instructions to the user
		    var instructions = document.createElement(\"p\");
		    instructions.textContent = \"The CSV file download has been initiated. \" +
		                               \"Please save it to this location:\n\" +
		                               \"CSV/IncomingCSV.csv\";
		    instructions.style.textAlign = \"center\";
		    instructions.style.marginTop = \"20px\";
		    document.body.appendChild(instructions);
		
		    alert('CSV file download has started. Please check your downloads and move the file to the correct location.');
		}

This actually works if I run this on Safari, but I want to keep it to a HTML prompt if possible.

I thought about using window.KeyboardMaestro.ProcessAppleScript to create the file, but the escaping required to program this in javascript within html within shell was enough to make a grown man cry. I couldn't figure it out.

Any help would be greatly appreciated!

I came up with something that is useful enough for me in the interim to finding out a more graceful solution that doesn't involve saving the data to a KM variable.

It is, however, almost guaranteed to make Peter Lewis violently ill if he finds out.

I've used:

window.KeyboardMaestro.Log( 'Message' )

in the HTML prompt to save the CSV data from the prompt to the engine log of Keyboard Maestro. I begin the macro by deleting the engine log using a standard KM "Delete File" action. In the code for the HTML window, I use the log command above as a stand-in for saving the data to a file like so:

window.KeyboardMaestro.Log('CSV_CONTENT_START\n' + csvString + '\nCSV_CONTENT_END')

this saves the CSV data as text on separate rows to the engine log, with the unique markers starting and ending the data so it can be easily identified and retrieved. I've then set up another macro to hold a Javascript that extracts this data from the engine log file and formats it as a CSV file. Below is the code, edit the file paths as necessary:

// Extract HTML CSV Data From Log

// Ensure we have access to the necessary Objective-C bridging.
ObjC.import('Foundation')

function kmVariable(name, value = null) {
    const km = Application('Keyboard Maestro Engine')
    if (value === null) {
        return km.getvariable(name)
    } else {
        km.setvariable(name, { to: value })
    }
}

function log(message) {
    if (kmVariable("CSVFromEngineLogLogWrite") === "true") {
        kmVariable("CSVFromEngineLogLog", kmVariable("CSVFromEngineLogLog") + "\n" + message);
    }
}

function writeCSV(filePath, content) {
    try {
        const nsString = $.NSString.alloc.initWithUTF8String(content);
        const data = nsString.dataUsingEncoding($.NSUTF8StringEncoding);
        const success = data.writeToFileAtomically(filePath, true);
        log(`Write operation result for ${filePath}: ${success}`);
        return success;
    } catch (error) {
        log(`Error writing CSV file ${filePath}: ${error.message}`);
        throw error;
    }
}

function fileExists(filePath) {
    return $.NSFileManager.defaultManager.fileExistsAtPath(filePath);
}

function deleteFile(filePath) {
    try {
        const fileManager = $.NSFileManager.defaultManager;
        const error = $();
        const success = fileManager.removeItemAtPathError(filePath, error);
        if (success) {
            log(`Deleted file: ${filePath}`);
        } else {
            log(`Error deleting file ${filePath}: ${ObjC.unwrap(error.value)}`);
        }
        return success;
    } catch (error) {
        log(`Error deleting file ${filePath}: ${error.message}`);
        return false;
    }
}

function processCSVFromLog() {
    const logPath = "[Replace with full file path]/Logs/Keyboard Maestro/Engine.log";
    const csvPath = "[Replace with full file path]/ExampleCSV.csv";

    try {
        log("Starting CSV processing");
        
        // Read the log file as plain text
        const fileManager = $.NSFileManager.defaultManager;
        const data = fileManager.contentsAtPath(logPath);
        const nsString = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);
        const logContent = ObjC.unwrap(nsString);
        
        log("Engine log read successfully");

        const startMarker = 'CSV_CONTENT_START\n';
        const endMarker = '\nCSV_CONTENT_END';
        const startIndex = logContent.lastIndexOf(startMarker);
        const endIndex = logContent.lastIndexOf(endMarker);

        if (startIndex === -1 || endIndex === -1) {
            throw new Error("CSV content markers not found in the log");
        }

        const csvContent = logContent.substring(startIndex + startMarker.length, endIndex);
        log("CSV content extracted from log");

        // Process the CSV content to maintain row and column structure
        const rows = csvContent.trim().split('\n');
        const processedCsvContent = rows.join('\n');

        // Only delete the existing file after we've successfully extracted the content
        if (fileExists(csvPath)) {
            if (!deleteFile(csvPath)) {
                throw new Error("Failed to delete existing CSV file");
            }
        }

        if (writeCSV(csvPath, processedCsvContent)) {
            log("CSV content successfully written to file");
            kmVariable("CSVFromEngineLogCompletionState", "true");
        } else {
            throw new Error("Failed to write CSV content to file");
        }

    } catch (error) {
        log(`Error processing CSV: ${error.message}`);
        kmVariable("CSVFromEngineLogCompletionState", "false");
    }
}

// Run the main function
processCSVFromLog();

This Javascript writes to these KM variables so you can what's going on / configure further actions:

CSVFromEngineLogLog - logs for this javascript.
CSVFromEngineLogLogWrite - set to true or false prior the script running depending if you want logs written to the above KM variable.
CSVFromEngineLogCompletionState - returns true or false depending if the script was successful or not.

How much data are you talking about? For giggles I've just read in, "Filter" reversed, and written out a 110MB log file and KM didn't even blink -- took 7 seconds on my old, spinning rust, iMac.

If you make sure to use a local variable then the data won't be in "the KM environment" after the macro completes anyway.

Here's how I handle the situation:

If the data can be passed as HTMLResult, then I return it there. Otherwise, I write a macro that takes the data in TriggerValue, and "Trigger" the macro from the custom HTML prompt.

Yes, I suppose technically the data is stored in a KM variable at some point for a short amount of time, but it goes away really quickly. And KM can handle huge strings, so it's just not an issue.