Skip to content

Instantly share code, notes, and snippets.

@heptal
Last active June 5, 2023 13:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heptal/0103e664cbdd7b552f7444503b8022b4 to your computer and use it in GitHub Desktop.
Save heptal/0103e664cbdd7b552f7444503b8022b4 to your computer and use it in GitHub Desktop.
Poor man's imitation of Postman in Hammerspoon

Exploring the possibilities with hs.webview and hs.webview.usercontent. The primary feature here is WKUserContentController, which allows for injecting user scripts into a web view and for JavaScript to post messages directly back to the native runtime.

The good thing is it's very easy to design the web portion in JS sketchbooks such as jsbin/codepen/jsfiddle because the snippet code structure maps cleanly to this. The bad part is that WKWebView doesn't always behave consistently or have a clean slate. Maybe this will help resolve state issues in the future. It will randomly not be able to find injected scripts without any code changes, and I haven't figured out why.

In this, a two-column table in HTML can be filled with values from Lua tables, and can also send them back. This is used for HTTP requests with arbitrary headers in a fun and exciting way.

callback = function(input)
  print(hs.inspect(input))
  local data = input.body; 
  if data.url then
    hs.http.asyncPost(data.url, data.payload, data.headers, function(status, body, headers)
        print(status,headers,body)
        view:evaluateJavaScript("document.getElementById('response').textContent = '"..body.."'")
      end)
  end
end;

name = hs.host.uuid():gsub("-","");
frame = hs.geometry.rect(hs.screen.mainScreen():frame().center, "500x700");
ucc = hs.webview.usercontent.new(name):setCallback(callback)
ucc:injectScript({source="function sendData(d) {webkit.messageHandlers."..name..".postMessage(d)}", mainFrame=false});
loadTable = function(t) for k,v in pairs(t) do view:evaluateJavaScript("addRow('"..k.."','"..v.."')") end end;

view = hs.webview.new(frame, {developerExtrasEnabled=true}, ucc):allowTextEntry(true):windowStyle(1|2|4|8);

html=[[

<button id="addRow">Add row</button>
<button id="sendTable">Send table</button>
<button id="sendPost">Send POST</button>
<table id="tableData">
  <tr>
    <th>Key</th>
    <th>Value</th>
  </tr>
</table>
<input id="url" type="text" placeholder="url">
<textarea id="payload" rows="4" cols="50" placeholder="request payload"></textarea>
<textarea id="response" rows="20" cols="50" placeholder="response"></textarea>

]]

js = [[

<script type="text/javascript">
function addRow(key, val) {
  var row = document.getElementById("tableData").insertRow();
  [key, val].forEach(function(v, i, a) {
    var cell = row.insertCell(); cell.contentEditable = true; cell.className = "dataCell"
    cell.textContent = ["string", "number"].includes(typeof v) ? v : ""
    cell.addEventListener("keydown", function(e) {e.keyCode === 13 && e.preventDefault()})
  })
}

function makeObj() {
  var data = {}, empty = [], cells = document.getElementsByClassName("dataCell")
  for (var i = 0; i < cells.length; i += 2) {
    var key = cells[i].textContent, val = cells[i + 1].textContent
    if (key === "" && val === "") {empty.push(cells[i]); continue}
    data[key] = val
  }
  empty.forEach(function(v, i, a) {v.parentNode.remove()})
  return data
}

function makePost() {
  return {
    headers: makeObj(),
    url: document.getElementById("url").value,
    payload: document.getElementById("payload").value
  }
}

window.onload = function() {
document.getElementById("addRow").onclick = addRow
document.getElementById("sendTable").onclick = function() {sendData(makeObj())}
document.getElementById("sendPost").onclick = function() {sendData(makePost())}
}
</script>

]]

css = [[

<style>
table { border-collapse: collapse; }
th, td { border: 2px solid black; width:200px; }
th { background: #cccccc;}
tr:nth-child(even) { background-color: #eeeeff; }
input { width: 400px; }
textarea, .dataCell { font-family: monospace; }
textarea, input { display: block; }
</style>

]]

view:html(html..js..css):show()
@dasmurphy
Copy link

dasmurphy commented Apr 20, 2020

I know this is an old post, but if you change it a litte bit, it would be working much better, since the injected script needs a name with a letter in front.

name = "m"..hs.host.uuid():gsub("-","");

And thanks for the source. It's very useful.

@luckman212
Copy link

@heptal This was super helpful to get me started with hs.webview! 🎉

You saved me many days of potential hair-pulling.
Thank you 🙏

@relipse
Copy link

relipse commented Jun 5, 2023

I tried this on Hammerspoon 0.9.100 (6815) but it wouldn't send the JavaScript commands back to hammerspoon. Does this still work?

--UPDATE-- I used dasmurphy's tip and now it seems to be working. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment