Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.