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:
		
							
								
								
									
										109
									
								
								xP/public/xP.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								xP/public/xP.css
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										188
									
								
								xP/public/xP.js
									
									
									
									
									
										Normal 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,
 | 
			
		||||
	})
 | 
			
		||||
})
 | 
			
		||||
		Reference in New Issue
	
	Block a user