packages feed

jsaddle-terminal-0.1.0.0: src/Language/Javascript/JSaddle/Terminal/Bootstrap.hs

{-# LANGUAGE OverloadedStrings #-}
-- | The JS side of the tunnel: a self-contained HTML page the IDE loads
-- into a per-pane iframe.  It embeds jsaddle's stock JS runtime
-- ('Language.Javascript.JSaddle.Run.Files') wired to @window.postMessage@:
--
-- * incoming batches arrive as @{leksahJsaddleBatch: key, batch: json}@
--   messages from the parent (the IDE page);
-- * async results go back as @{leksahJsaddle: key, type:'results',
--   seq: n, data: json}@ — @seq@ lets the IDE restore order (its jsaddle
--   callbacks run on forked threads);
-- * sync callbacks use a __synchronous__ XHR to the IDE's own local web
--   server (same origin), which answers by round-tripping over the pane's
--   stdin/stdout — the app machine itself needs no reachable socket;
-- * @{type:'ready'}@ is posted once the runtime is installed, and the IDE
--   only then ACKs the exe's HELLO.
module Language.Javascript.JSaddle.Terminal.Bootstrap
  ( bootstrapHtml
  , bootstrapJs
  , syncPath
  , framePath
  ) where

import Data.ByteString.Lazy (ByteString)
import Language.Javascript.JSaddle.Run.Files (initState, runBatch, ghcjsHelpers)

-- | URL path (on the IDE's own server) answering sync XHRs for a pane key.
syncPath :: ByteString -> ByteString
syncPath key = "/jsaddle-terminal/sync/" <> key

-- | URL path (on the IDE's own server) serving 'bootstrapHtml' for a key.
framePath :: ByteString -> ByteString
framePath key = "/jsaddle-terminal/frame/" <> key

-- | The iframe page for pane @key@ (already URL/JS-safe: keys are made of
-- @[A-Za-z0-9._-]@ by the IDE's sanitiser).
bootstrapHtml :: ByteString -> ByteString
bootstrapHtml key =
  "<!DOCTYPE html>\n\
  \<html>\n\
  \<head>\n\
  \<meta charset=\"utf-8\">\n\
  \<title>jsaddle-terminal</title>\n\
  \<style>html,body{margin:0;padding:0;width:100%;height:100%;\
  \background:rgb(16,16,16);color:#fff;\
  \font-family:-apple-system,BlinkMacSystemFont,sans-serif}</style>\n\
  \</head>\n\
  \<body>\n\
  \<script>\n" <> bootstrapJs key <> "</script>\n\
  \</body>\n\
  \</html>\n"

-- | The runtime script for pane @key@.
bootstrapJs :: ByteString -> ByteString
bootstrapJs key =
  ghcjsHelpers <>
  "var jstermKey = '" <> key <> "';\n\
  \var jstermSeq = 0;\n\
  \function jstermSend(r){\n\
  \  parent.postMessage({leksahJsaddle: jstermKey, type: 'results',\n\
  \                      seq: jstermSeq++, data: JSON.stringify(r)}, '*');\n\
  \}\n\
  \function jstermSendSync(r){\n\
  \  var xhr = new XMLHttpRequest();\n\
  \  xhr.open('POST', '" <> syncPath key <> "', false);\n\
  \  xhr.send(JSON.stringify(r));\n\
  \  if (xhr.status !== 200) { throw new Error('jsaddle-terminal sync failed: ' + xhr.status); }\n\
  \  return JSON.parse(xhr.responseText);\n\
  \}\n\
  \window.runJSaddleBatch = (function() {\n" <>
  initState <>
  "\n return function(batch) {\n" <>
  runBatch (\a -> "jstermSend(" <> a <> ");")
           (Just (\a -> "jstermSendSync(" <> a <> ")")) <>
  "\n };\n\
  \})();\n\
  \window.addEventListener('message', function(e){\n\
  \  if (e.data && e.data.leksahJsaddleBatch === jstermKey) {\n\
  \    runJSaddleBatch(JSON.parse(e.data.batch));\n\
  \  }\n\
  \});\n\
  \function jstermFocused(){\n\
  \  parent.postMessage({leksahJsaddle: jstermKey, type: 'focus'}, '*');\n\
  \}\n\
  \window.addEventListener('focus', jstermFocused);\n\
  \window.addEventListener('mousedown', jstermFocused, true);\n\
  \\n\
  \// Forward modifier-key chords to the host so its global keymap (build,\n\
  \// find, the ⌃` flipper, ⌘-navigation, …) works while this pane has focus —\n\
  \// keyboard events don't cross the frame boundary on their own.  Plain keys\n\
  \// (no modifier) stay in the app; we never preventDefault, so the app's own\n\
  \// ⌘C/⌘V etc. keep working (the host acts only on chords it recognises).\n\
  \function jstermFwdKey(kind, e){\n\
  \  if (e.ctrlKey || e.metaKey || e.altKey ||\n\
  \      e.key === 'Control' || e.key === 'Meta' || e.key === 'Alt') {\n\
  \    parent.postMessage({leksahJsaddle: jstermKey, type: 'key', kind: kind,\n\
  \      keyCode: e.keyCode, key: e.key, code: e.code,\n\
  \      ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey, meta: e.metaKey}, '*');\n\
  \  }\n\
  \}\n\
  \window.addEventListener('keydown', function(e){ jstermFwdKey('keydown', e); }, true);\n\
  \window.addEventListener('keyup',   function(e){ jstermFwdKey('keyup', e); }, true);\n\
  \parent.postMessage({leksahJsaddle: jstermKey, type: 'ready'}, '*');\n"