From 1639235a48dbed75c2563c9a497b41c31a2a1bae Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Mon, 8 Aug 2022 04:39:20 +0200 Subject: 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. --- xP/public/xP.js | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 xP/public/xP.js (limited to 'xP/public/xP.js') 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, + }) +}) -- cgit v1.2.3-54-g00ecf