diff options
| -rw-r--r-- | xC-gen-proto-go.awk | 3 | ||||
| -rw-r--r-- | xC-proto | 2 | ||||
| -rw-r--r-- | xP/public/xP.js | 293 | 
3 files changed, 218 insertions, 80 deletions
| diff --git a/xC-gen-proto-go.awk b/xC-gen-proto-go.awk index 1880de7..1a64eb8 100644 --- a/xC-gen-proto-go.awk +++ b/xC-gen-proto-go.awk @@ -382,7 +382,8 @@ function codegen_union(name, cg,    gotype, tagfield, tagvar) {  	print "}"  	print "" -	print "func (u *" gotype ") MarshalJSON() ([]byte, error) {" +	# This cannot be a pointer method, it wouldn't work recursively. +	print "func (u " gotype ") MarshalJSON() ([]byte, error) {"  	print "\treturn json.Marshal(u.Interface)"  	print "}"  	print "" @@ -104,6 +104,8 @@ struct EventMessage {  		} items<>;  	case BUFFER_CLEAR:  		string buffer_name; + +	// Restriction: command_seq is strictly increasing, across both of these.  	case ERROR:  		u32 command_seq;  		string error; diff --git a/xP/public/xP.js b/xP/public/xP.js index f21683c..fd3a0ae 100644 --- a/xP/public/xP.js +++ b/xP/public/xP.js @@ -1,5 +1,175 @@  // Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>  // SPDX-License-Identifier: 0BSD +'use strict' + +// ---- RPC -------------------------------------------------------------------- + +class RelayRpc extends EventTarget { +	constructor(url) { +		super() +		this.url = url +		this.commandSeq = 0 +	} + +	connect() { +		// We can't close the connection immediately, as that queues a task. +		if (this.ws !== undefined) +			throw "Already connecting or connected" + +		return new Promise((resolve, reject) => { +			let ws = this.ws = new WebSocket(this.url) +			ws.onopen = event => { +				this._initialize() +				resolve() +			} +			// It's going to be code 1006 with no further info. +			ws.onclose = event => { +				reject() +				this.ws = undefined +			} +		}) +	} + +	_initialize() { +		this.ws.onopen = undefined +		this.ws.onmessage = event => { +			this._process(event.data) +		} +		this.ws.onerror = event => { +			this.dispatchEvent(new CustomEvent('error')) +		} +		this.ws.onclose = event => { +			let message = "Connection closed: " + +				event.code + " (" + event.reason + ")" +			for (const seq in this.promised) +				this.promised[seq].reject(message) + +			this.ws = undefined +			this.dispatchEvent(new CustomEvent('close', { +				detail: {message, code: event.code, reason: event.reason}, +			})) + +			// Now connect() can be called again. +		} + +		this.promised = {} +	} + +	_process(data) { +		console.log(data) + +		if (typeof data !== 'string') +			throw "Binary messages not supported" + +		let message = JSON.parse(data) +		if (typeof message !== 'object') +			throw "Invalid message" +		let e = message.data +		if (typeof e !== 'object') +			throw "Invalid message" + +		switch (e.event) { +		case 'Error': +			if (this.promised[e.commandSeq] !== undefined) +				this.promised[e.commandSeq].reject(e.error) +			else +				console.error("Unawaited error") +			break +		case 'Response': +			if (this.promised[e.commandSeq] !== undefined) +				this.promised[e.commandSeq].resolve(e.data) +			else +				console.error("Unawaited response") +			break +		default: +			if (typeof e.event !== 'string') +				throw "Invalid event tag" + +			this.dispatchEvent(new CustomEvent(e.event, {detail: e})) + +			// Minor abstraction layering violation. +			m.redraw() +			return +		} + +		delete this.promised[e.commandSeq] +		for (const seq in this.promised) { +			// We don't particularly care about wraparound issues. +			if (seq >= e.commandSeq) +				continue + +			this.promised[seq].reject("No response") +			delete this.promised[seq] +		} +	} + +	send(params) { +		if (this.ws === undefined) +			throw "Not connected" +		if (typeof params !== 'object') +			throw "Method parameters must be an object" + +		let seq = ++this.commandSeq +		if (seq >= 1 << 32) +			seq = this.commandSeq = 0 + +		this.ws.send(JSON.stringify({commandSeq: seq, data: params})) +		return new Promise((resolve, reject) => { +			this.promised[seq] = {resolve, reject} +		}) +	} +} + +// ---- Event processing ------------------------------------------------------- + +// TODO: Probably reset state on disconnect, and indicate to user. +let rpc = new RelayRpc(proxy) +rpc.connect() +	.then(result => { +		rpc.send({command: 'Hello', version: 1}) +	}) + +let buffers = new Map() +let bufferCurrent = undefined + +rpc.addEventListener('BufferUpdate', event => { +	let e = event.detail, b = buffers.get(e.bufferName) +	if (b === undefined) { +		b = {lines: []} +		buffers.set(e.bufferName, b) +	} +	// TODO: Update any buffer properties. +}) + +rpc.addEventListener('BufferRename', event => { +	let e = event.detail +	buffers.set(e.new, buffers.get(e.bufferName)) +	buffers.delete(e.bufferName) +}) + +rpc.addEventListener('BufferRemove', event => { +	let e = event.detail +	buffers.delete(e.bufferName) +}) + +rpc.addEventListener('BufferActivate', event => { +	let e = event.detail +	bufferCurrent = e.bufferName +	// TODO: Somehow scroll to the end of it immediately. +	// TODO: Focus the textarea. +}) + +rpc.addEventListener('BufferLine', event => { +	let e = event.detail, b = buffers.get(e.bufferName) +	if (b !== undefined) +		b.lines.push({when: e.when, rendition: e.rendition, items: e.items}) +}) + +rpc.addEventListener('BufferClear', event => { +	let e = event.detail, b = buffers.get(e.bufferName) +	if (b !== undefined) +		b.lines.length = 0 +})  // --- Colours ----------------------------------------------------------------- @@ -33,69 +203,6 @@ function applyColor(fg, bg, inverse) {  		return style  } -// ---- Event processing ------------------------------------------------------- - -// 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() -} -  // ---- UI ---------------------------------------------------------------------  let BufferList = { @@ -103,8 +210,8 @@ let BufferList = {  		let items = []  		buffers.forEach((b, name) => {  			let attrs = { -				onclick: e => { -					send({command: 'BufferActivate', bufferName: name}) +				onclick: event => { +					rpc.send({command: 'BufferActivate', bufferName: name})  				},  			}  			if (name == bufferCurrent) @@ -213,26 +320,54 @@ let Buffer = {  	},  } +function onKeyDown(event) { +	// TODO: And perhaps on other actions, too. +	rpc.send({command: 'Active'}) + +	// TODO: Cancel any current autocomplete. + +	let textarea = event.currentTarget +	switch (event.keyCode) { +	case 9: +		if (textarea.selectionStart !== textarea.selectionEnd) +			return +		rpc.send({ +			command: 'BufferComplete', +			bufferName: bufferCurrent, +			text: textarea.value, +			position: textarea.selectionEnd, +		}).then(response => { +			// TODO: Somehow display remaining options, or cycle through. +			if (response.completions.length) +				textarea.setRangeText(response.completions[0], +					response.start, textarea.selectionEnd, 'end') +			if (response.completions.length === 1) +				textarea.setRangeText(' ', +					textarea.selectionStart, textarea.selectionEnd, 'end') +		}) +		break; +	case 13: +		rpc.send({ +			command: 'BufferInput', +			bufferName: bufferCurrent, +			text: textarea.value, +		}) +		textarea.value = '' +		break; +	default: +		return +	} + +	event.preventDefault() +} +  // 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 = '' -			}, +			onkeydown: onKeyDown,  		})  	},  } | 
