summaryrefslogtreecommitdiff
path: root/xP/public/xP.js
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2022-08-08 04:39:20 +0200
committerPřemysl Eric Janouch <p@janouch.name>2022-09-05 14:26:00 +0200
commit1639235a48dbed75c2563c9a497b41c31a2a1bae (patch)
tree18193b72fa47e6bcac1358289ac9c36ed00c70ac /xP/public/xP.js
parent2160d037943ef0a3adbf4c6e30a91ee0f205c3f3 (diff)
downloadxK-1639235a48dbed75c2563c9a497b41c31a2a1bae.tar.gz
xK-1639235a48dbed75c2563c9a497b41c31a2a1bae.tar.xz
xK-1639235a48dbed75c2563c9a497b41c31a2a1bae.zip
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options, it was decided to implement an XDR-like protocol code generator in portable AWK. It now has two backends, per each of: - xF, the X11 frontend, is in C, and is meant to be the primary user interface in the future. - xP, the web frontend, relies on a protocol proxy written in Go, and is meant for use on-the-go (no pun intended). They are very much work-in-progress proofs of concept right now, and the relay protocol is certain to change.
Diffstat (limited to 'xP/public/xP.js')
-rw-r--r--xP/public/xP.js188
1 files changed, 188 insertions, 0 deletions
diff --git a/xP/public/xP.js b/xP/public/xP.js
new file mode 100644
index 0000000..4eebb7a
--- /dev/null
+++ b/xP/public/xP.js
@@ -0,0 +1,188 @@
+// 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()
+}
+
+let BufferList = {
+ view: vnode => {
+ let items = []
+ buffers.forEach((b, name) => {
+ let attrs = {
+ onclick: e => {
+ send({command: 'BufferActivate', bufferName: name})
+ },
+ }
+ if (name == bufferCurrent)
+ attrs.class = 'active'
+ items.push(m('.item', attrs, name))
+ })
+ return m('.list', {}, items)
+ },
+}
+
+let Content = {
+ view: vnode => {
+ let line = vnode.children[0]
+ let content = []
+ switch (line.rendition) {
+ case 'Indent': content.push(m('span.mark', {}, '')); break
+ case 'Status': content.push(m('span.mark', {}, '–')); break
+ case 'Error': content.push(m('span.mark.error', {}, '⚠')); break
+ case 'Join': content.push(m('span.mark.join', {}, '→')); break
+ case 'Part': content.push(m('span.mark.part', {}, '←')); break
+ }
+
+ let classes = new Set()
+ let flip = c => {
+ if (classes.has(c))
+ classes.delete(c)
+ else
+ classes.add(c)
+ }
+ line.items.forEach(item => {
+ // TODO: Colours.
+ switch (item.kind) {
+ case 'Text':
+ // TODO: Detect and transform links.
+ content.push(m('span', {
+ class: Array.from(classes.keys()).join(' '),
+ }, item.text))
+ break
+ case 'Reset':
+ classes.clear()
+ break
+ case 'FlipBold': flip('b'); break
+ case 'FlipItalic': flip('i'); break
+ case 'FlipUnderline': flip('u'); break
+ case 'FlipInverse': flip('i'); break
+ case 'FlipCrossedOut': flip('s'); break
+ case 'FlipMonospace': flip('m'); break
+ }
+ })
+ return m('.content', {}, content)
+ },
+}
+
+let Buffer = {
+ view: vnode => {
+ let lines = []
+ let b = buffers.get(bufferCurrent)
+ if (b === undefined)
+ return
+
+ let lastDateMark = undefined
+ b.lines.forEach(line => {
+ let date = new Date(line.when * 1000)
+ let dateMark = date.toLocaleDateString()
+ if (dateMark !== lastDateMark) {
+ lines.push(m('.date', {}, dateMark))
+ lastDateMark = dateMark
+ }
+
+ lines.push(m('.time', {}, date.toLocaleTimeString()))
+ lines.push(m(Content, {}, line))
+ })
+ return m('.buffer-container', {}, [
+ m('.filler'),
+ m('.buffer', {}, lines),
+ ])
+ },
+}
+
+// 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 = ''
+ },
+ })
+ },
+}
+
+let Main = {
+ view: vnode => {
+ return m('.xP', {}, [
+ m('.title', {}, "xP"),
+ m('.middle', {}, [m(BufferList), m(Buffer)]),
+ m('.status', {}, bufferCurrent),
+ m(Input),
+ ])
+ },
+}
+
+// TODO: Buffer names should work as routes.
+window.addEventListener('load', () => {
+ m.route(document.body, '/', {
+ '/': Main,
+ })
+})