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.css | 109 ++++++++++++++++++++++++++++++++ xP/public/xP.js | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 xP/public/xP.css create mode 100644 xP/public/xP.js (limited to 'xP/public') diff --git a/xP/public/xP.css b/xP/public/xP.css new file mode 100644 index 0000000..9a98c13 --- /dev/null +++ b/xP/public/xP.css @@ -0,0 +1,109 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} + +.xP { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.title, .status { + background: #f8f8f8; + border-bottom: 1px solid #ccc; + padding: .05rem .3rem; +} + +.middle { + flex: auto; + display: flex; + flex-direction: row; + overflow: hidden; +} + +.list { + overflow-y: auto; + border-right: 1px solid #ccc; + min-width: 10rem; +} +.item { + padding: .05rem .3rem; + cursor: default; +} +.item.active { + font-weight: bold; +} + +/* Only Firefox currently supports align-content: safe end, thus this. */ +.buffer-container { + flex: auto; + display: flex; + flex-direction: column; + overflow: hidden; +} +.filler { + flex: auto; +} +.buffer { + display: grid; + grid-template-columns: max-content auto; + overflow-y: auto; +} + +.date { + padding: .3rem; + grid-column: span 2; + font-weight: bold; +} +.time { + padding: .1rem .3rem; + background: #f8f8f8; + color: #bbb; + border-right: 1px solid #ccc; +} +.mark { + padding-right: .3rem; + text-align: center; + display: inline-block; + min-width: 2rem; +} +.mark.error { + color: red; +} +.mark.join { + color: green; +} +.mark.part { + color: red; +} +.content { + padding: .1rem .3rem; + white-space: pre-wrap; +} +.content span.b { + font-weight: bold; +} +.content span.i { + font-style: italic; +} +.content span.u { + text-decoration: underline; +} +.content span.s { + text-decoration: line-through; +} +.content span.m { + font-family: monospace; +} + +.status { + border-top: 2px solid #fff; +} + +textarea { + padding: .05rem .3rem; + font-family: inherit; +} 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-70-g09d2