summaryrefslogtreecommitdiff
path: root/xP/public
diff options
context:
space:
mode:
Diffstat (limited to 'xP/public')
-rw-r--r--xP/public/xP.js293
1 files changed, 214 insertions, 79 deletions
diff --git a/xP/public/xP.js b/xP/public/xP.js
index f21683c..fd3a0ae 100644
--- a/xP/public/xP.js
+++ b/xP/public/xP.js
@@ -1,5 +1,175 @@
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
+'use strict'
+
+// ---- RPC --------------------------------------------------------------------
+
+class RelayRpc extends EventTarget {
+ constructor(url) {
+ super()
+ this.url = url
+ this.commandSeq = 0
+ }
+
+ connect() {
+ // We can't close the connection immediately, as that queues a task.
+ if (this.ws !== undefined)
+ throw "Already connecting or connected"
+
+ return new Promise((resolve, reject) => {
+ let ws = this.ws = new WebSocket(this.url)
+ ws.onopen = event => {
+ this._initialize()
+ resolve()
+ }
+ // It's going to be code 1006 with no further info.
+ ws.onclose = event => {
+ reject()
+ this.ws = undefined
+ }
+ })
+ }
+
+ _initialize() {
+ this.ws.onopen = undefined
+ this.ws.onmessage = event => {
+ this._process(event.data)
+ }
+ this.ws.onerror = event => {
+ this.dispatchEvent(new CustomEvent('error'))
+ }
+ this.ws.onclose = event => {
+ let message = "Connection closed: " +
+ event.code + " (" + event.reason + ")"
+ for (const seq in this.promised)
+ this.promised[seq].reject(message)
+
+ this.ws = undefined
+ this.dispatchEvent(new CustomEvent('close', {
+ detail: {message, code: event.code, reason: event.reason},
+ }))
+
+ // Now connect() can be called again.
+ }
+
+ this.promised = {}
+ }
+
+ _process(data) {
+ console.log(data)
+
+ if (typeof data !== 'string')
+ throw "Binary messages not supported"
+
+ let message = JSON.parse(data)
+ if (typeof message !== 'object')
+ throw "Invalid message"
+ let e = message.data
+ if (typeof e !== 'object')
+ throw "Invalid message"
+
+ switch (e.event) {
+ case 'Error':
+ if (this.promised[e.commandSeq] !== undefined)
+ this.promised[e.commandSeq].reject(e.error)
+ else
+ console.error("Unawaited error")
+ break
+ case 'Response':
+ if (this.promised[e.commandSeq] !== undefined)
+ this.promised[e.commandSeq].resolve(e.data)
+ else
+ console.error("Unawaited response")
+ break
+ default:
+ if (typeof e.event !== 'string')
+ throw "Invalid event tag"
+
+ this.dispatchEvent(new CustomEvent(e.event, {detail: e}))
+
+ // Minor abstraction layering violation.
+ m.redraw()
+ return
+ }
+
+ delete this.promised[e.commandSeq]
+ for (const seq in this.promised) {
+ // We don't particularly care about wraparound issues.
+ if (seq >= e.commandSeq)
+ continue
+
+ this.promised[seq].reject("No response")
+ delete this.promised[seq]
+ }
+ }
+
+ send(params) {
+ if (this.ws === undefined)
+ throw "Not connected"
+ if (typeof params !== 'object')
+ throw "Method parameters must be an object"
+
+ let seq = ++this.commandSeq
+ if (seq >= 1 << 32)
+ seq = this.commandSeq = 0
+
+ this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
+ return new Promise((resolve, reject) => {
+ this.promised[seq] = {resolve, reject}
+ })
+ }
+}
+
+// ---- Event processing -------------------------------------------------------
+
+// TODO: Probably reset state on disconnect, and indicate to user.
+let rpc = new RelayRpc(proxy)
+rpc.connect()
+ .then(result => {
+ rpc.send({command: 'Hello', version: 1})
+ })
+
+let buffers = new Map()
+let bufferCurrent = undefined
+
+rpc.addEventListener('BufferUpdate', event => {
+ let e = event.detail, b = buffers.get(e.bufferName)
+ if (b === undefined) {
+ b = {lines: []}
+ buffers.set(e.bufferName, b)
+ }
+ // TODO: Update any buffer properties.
+})
+
+rpc.addEventListener('BufferRename', event => {
+ let e = event.detail
+ buffers.set(e.new, buffers.get(e.bufferName))
+ buffers.delete(e.bufferName)
+})
+
+rpc.addEventListener('BufferRemove', event => {
+ let e = event.detail
+ buffers.delete(e.bufferName)
+})
+
+rpc.addEventListener('BufferActivate', event => {
+ let e = event.detail
+ bufferCurrent = e.bufferName
+ // TODO: Somehow scroll to the end of it immediately.
+ // TODO: Focus the textarea.
+})
+
+rpc.addEventListener('BufferLine', event => {
+ let e = event.detail, b = buffers.get(e.bufferName)
+ if (b !== undefined)
+ b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
+})
+
+rpc.addEventListener('BufferClear', event => {
+ let e = event.detail, b = buffers.get(e.bufferName)
+ if (b !== undefined)
+ b.lines.length = 0
+})
// --- Colours -----------------------------------------------------------------
@@ -33,69 +203,6 @@ function applyColor(fg, bg, inverse) {
return style
}
-// ---- Event processing -------------------------------------------------------
-
-// TODO: Probably reset state on disconnect, and indicate to user.
-let socket = new WebSocket(proxy)
-
-let commandSeq = 0
-function send(command) {
- socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command}))
-}
-
-socket.onopen = function(event) {
- send({command: 'Hello', version: 1})
-}
-
-let buffers = new Map()
-let bufferCurrent = undefined
-
-socket.onmessage = function(event) {
- console.log(event.data)
-
- let e = JSON.parse(event.data).data
- switch (e.event) {
- case 'BufferUpdate':
- {
- let b = buffers.get(e.bufferName)
- if (b === undefined) {
- b = {lines: []}
- buffers.set(e.bufferName, b)
- }
- // TODO: Update any buffer properties.
- break
- }
- case 'BufferRename':
- buffers.set(e.new, buffers.get(e.bufferName))
- buffers.delete(e.bufferName)
- break
- case 'BufferRemove':
- buffers.delete(e.bufferName)
- break
- case 'BufferActivate':
- bufferCurrent = e.bufferName
- // TODO: Somehow scroll to the end of it immediately.
- // TODO: Focus the textarea.
- break
- case 'BufferLine':
- {
- let b = buffers.get(e.bufferName)
- if (b !== undefined)
- b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
- break
- }
- case 'BufferClear':
- {
- let b = buffers.get(e.bufferName)
- if (b !== undefined)
- b.lines.length = 0
- break
- }
- }
-
- m.redraw()
-}
-
// ---- UI ---------------------------------------------------------------------
let BufferList = {
@@ -103,8 +210,8 @@ let BufferList = {
let items = []
buffers.forEach((b, name) => {
let attrs = {
- onclick: e => {
- send({command: 'BufferActivate', bufferName: name})
+ onclick: event => {
+ rpc.send({command: 'BufferActivate', bufferName: name})
},
}
if (name == bufferCurrent)
@@ -213,26 +320,54 @@ let Buffer = {
},
}
+function onKeyDown(event) {
+ // TODO: And perhaps on other actions, too.
+ rpc.send({command: 'Active'})
+
+ // TODO: Cancel any current autocomplete.
+
+ let textarea = event.currentTarget
+ switch (event.keyCode) {
+ case 9:
+ if (textarea.selectionStart !== textarea.selectionEnd)
+ return
+ rpc.send({
+ command: 'BufferComplete',
+ bufferName: bufferCurrent,
+ text: textarea.value,
+ position: textarea.selectionEnd,
+ }).then(response => {
+ // TODO: Somehow display remaining options, or cycle through.
+ if (response.completions.length)
+ textarea.setRangeText(response.completions[0],
+ response.start, textarea.selectionEnd, 'end')
+ if (response.completions.length === 1)
+ textarea.setRangeText(' ',
+ textarea.selectionStart, textarea.selectionEnd, 'end')
+ })
+ break;
+ case 13:
+ rpc.send({
+ command: 'BufferInput',
+ bufferName: bufferCurrent,
+ text: textarea.value,
+ })
+ textarea.value = ''
+ break;
+ default:
+ return
+ }
+
+ event.preventDefault()
+}
+
// TODO: This should be remembered across buffer switches,
// and we'll probably have to intercept /all/ key presses.
let Input = {
view: vnode => {
return m('textarea', {
rows: 1,
- onkeydown: e => {
- // TODO: And perhaps on other actions, too.
- send({command: 'Active'})
- if (e.keyCode !== 13)
- return
-
- send({
- command: 'BufferInput',
- bufferName: bufferCurrent,
- text: e.currentTarget.value,
- })
- e.preventDefault()
- e.currentTarget.value = ''
- },
+ onkeydown: onKeyDown,
})
},
}