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.
This commit is contained in:
2022-08-08 04:39:20 +02:00
parent 2160d03794
commit 1639235a48
20 changed files with 2798 additions and 91 deletions

109
xP/public/xP.css Normal file
View File

@@ -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;
}

188
xP/public/xP.js Normal file
View File

@@ -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,
})
})