summaryrefslogtreecommitdiff
path: root/xP/public
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
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')
-rw-r--r--xP/public/xP.css109
-rw-r--r--xP/public/xP.js188
2 files changed, 297 insertions, 0 deletions
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,
+ })
+})