Compare commits
	
		
			7 Commits
		
	
	
		
			93b66b6a26
			...
			2b13f891c9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2b13f891c9 | |||
| d55402234c | |||
| e3149b9abf | |||
| 976e7bfbb4 | |||
| 5fd76ba6f9 | |||
| 41878a587f | |||
| 80089a4d65 | 
							
								
								
									
										50
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								xC.c
									
									
									
									
									
								
							| @ -2868,6 +2868,7 @@ relay_try_fetch_client (struct app_context *ctx, int listen_fd) | ||||
| 		if (accept_error_is_transient (errno)) | ||||
| 			print_warning ("%s: %s", "accept", strerror (errno)); | ||||
| 		else | ||||
| 			// TODO: Rather dispose of the listening socket.
 | ||||
| 			print_fatal ("%s: %s", "accept", strerror (errno)); | ||||
| 		return true; | ||||
| 	} | ||||
| @ -12053,6 +12054,23 @@ handle_command_plugin (struct handler_args *a) | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| static bool | ||||
| handle_command_relay (struct handler_args *a) | ||||
| { | ||||
| 	if (*a->arguments) | ||||
| 		return false; | ||||
| 
 | ||||
| 	int len = 0; | ||||
| 	LIST_FOR_EACH (struct client, c, a->ctx->clients) | ||||
| 		len++; | ||||
| 
 | ||||
| 	if (a->ctx->relay_fd == -1) | ||||
| 		log_global_status (a->ctx, "The relay is not enabled"); | ||||
| 	else | ||||
| 		log_global_status (a->ctx, "The relay has #d clients", len); | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| static bool | ||||
| show_aliases_list (struct app_context *ctx) | ||||
| { | ||||
| @ -12789,6 +12807,9 @@ g_command_handlers[] = | ||||
| 	{ "plugin",     "Manage plugins", | ||||
| 	  "list | load <name> | unload <name>", | ||||
| 	  handle_command_plugin,     0 }, | ||||
| 	{ "relay",      "Show relay information", | ||||
| 	  NULL, | ||||
| 	  handle_command_relay,      0 }, | ||||
| 
 | ||||
| 	{ "alias",      "List or set aliases", | ||||
| 	  "[<name> <definition>]", | ||||
| @ -13887,22 +13908,27 @@ build_editor_command (struct app_context *ctx, const char *filename) | ||||
| 		{ | ||||
| 		case 'F': | ||||
| 			str_append (&argument, filename); | ||||
| 			break; | ||||
| 			continue; | ||||
| 		case 'L': | ||||
| 			str_append_printf (&argument, "%zu", line_one_based); | ||||
| 			break; | ||||
| 			continue; | ||||
| 		case 'C': | ||||
| 			str_append_printf (&argument, "%zu", column + 1); | ||||
| 			break; | ||||
| 			continue; | ||||
| 		case 'B': | ||||
| 			str_append_printf (&argument, "%d",  cursor + 1); | ||||
| 			break; | ||||
| 			continue; | ||||
| 		case '%': | ||||
| 		case ' ': | ||||
| 			str_append_c (&argument, *editor); | ||||
| 			break; | ||||
| 		default: | ||||
| 			print_warning ("unknown substitution variable"); | ||||
| 			continue; | ||||
| 		} | ||||
| 
 | ||||
| 		const char *p = editor; | ||||
| 		if (soft_assert (utf8_decode (&p, strlen (p)) > 0)) | ||||
| 		{ | ||||
| 			log_global_error (ctx, "Unknown substitution variable: %#&s", | ||||
| 				xstrndup (editor, p - editor)); | ||||
| 		} | ||||
| 	} | ||||
| 	if (argument.len) | ||||
| @ -14755,7 +14781,8 @@ try_reap_child (struct app_context *ctx) | ||||
| 	if (WIFSTOPPED (status)) | ||||
| 	{ | ||||
| 		// We could also send SIGCONT but what's the point
 | ||||
| 		print_debug ("a child has been stopped, killing its process group"); | ||||
| 		log_global_debug (ctx, | ||||
| 			"A child has been stopped, killing its process group"); | ||||
| 		kill (-zombie, SIGKILL); | ||||
| 		return true; | ||||
| 	} | ||||
| @ -15253,7 +15280,7 @@ client_process_message (struct client *c, | ||||
| 	if (!relay_command_message_deserialize (m, r) | ||||
| 	 || msg_unpacker_get_available (r)) | ||||
| 	{ | ||||
| 		print_error ("deserialization failed, killing client"); | ||||
| 		log_global_error (c->ctx, "Deserialization failed, killing client"); | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| @ -15272,7 +15299,8 @@ client_process_message (struct client *c, | ||||
| 		if (m->data.hello.version != RELAY_VERSION) | ||||
| 		{ | ||||
| 			// TODO: This should send back an error message and shut down.
 | ||||
| 			print_error ("protocol version mismatch, killing client"); | ||||
| 			log_global_error (c->ctx, | ||||
| 				"Protocol version mismatch, killing client"); | ||||
| 			return false; | ||||
| 		} | ||||
| 		c->initialized = true; | ||||
| @ -15300,7 +15328,7 @@ client_process_message (struct client *c, | ||||
| 		client_process_buffer_log (c, m->command_seq, buffer); | ||||
| 		break; | ||||
| 	default: | ||||
| 		print_warning ("unhandled client command"); | ||||
| 		log_global_debug (c->ctx, "Unhandled client command"); | ||||
| 		relay_prepare_error (c->ctx, m->command_seq, "Unknown command"); | ||||
| 		relay_send (c); | ||||
| 	} | ||||
|  | ||||
| @ -16,6 +16,10 @@ body { | ||||
| 	border-bottom: 1px solid #ccc; | ||||
| 	padding: .05rem .3rem; | ||||
| } | ||||
| .title { | ||||
| 	display: flex; | ||||
| 	justify-content: space-between; | ||||
| } | ||||
| 
 | ||||
| .middle { | ||||
| 	flex: auto; | ||||
| @ -52,6 +56,12 @@ body { | ||||
| 	grid-template-columns: max-content auto; | ||||
| 	overflow-y: auto; | ||||
| } | ||||
| .log { | ||||
| 	padding: .1rem .3rem; | ||||
| 	font-family: monospace; | ||||
| 	white-space: pre-wrap; | ||||
| 	overflow-y: auto; | ||||
| } | ||||
| 
 | ||||
| .date { | ||||
| 	padding: .3rem; | ||||
|  | ||||
							
								
								
									
										252
									
								
								xP/public/xP.js
									
									
									
									
									
								
							
							
						
						
									
										252
									
								
								xP/public/xP.js
									
									
									
									
									
								
							| @ -118,6 +118,11 @@ class RelayRpc extends EventTarget { | ||||
| 			this.promised[seq] = {resolve, reject} | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	base64decode(str) { | ||||
| 		return decodeURIComponent(atob(str).split('').map(c => | ||||
| 			'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ---- Event processing -------------------------------------------------------
 | ||||
| @ -126,10 +131,16 @@ let rpc = new RelayRpc(proxy) | ||||
| 
 | ||||
| let buffers = new Map() | ||||
| let bufferCurrent = undefined | ||||
| let bufferLog = undefined | ||||
| let bufferAutoscroll = true | ||||
| 
 | ||||
| let connecting = true | ||||
| rpc.connect().then(result => { | ||||
| 	buffers.clear() | ||||
| 	bufferCurrent = undefined | ||||
| 	bufferLog = undefined | ||||
| 	bufferAutoscroll = true | ||||
| 
 | ||||
| 	rpc.send({command: 'Hello', version: 1}) | ||||
| 	connecting = false | ||||
| 	m.redraw() | ||||
| @ -163,13 +174,24 @@ rpc.addEventListener('BufferRemove', event => { | ||||
| }) | ||||
| 
 | ||||
| rpc.addEventListener('BufferActivate', event => { | ||||
| 	let e = event.detail | ||||
| 	let e = event.detail, b = buffers.get(e.bufferName) | ||||
| 	let old = buffers.get(bufferCurrent) | ||||
| 	bufferCurrent = e.bufferName | ||||
| 	setTimeout(() => { | ||||
| 		let el = document.getElementById('input') | ||||
| 		if (el !== null) | ||||
| 			el.focus() | ||||
| 	}) | ||||
| 	bufferLog = undefined | ||||
| 	bufferAutoscroll = true | ||||
| 
 | ||||
| 	let textarea = document.getElementById('input') | ||||
| 	if (textarea === null) | ||||
| 		return | ||||
| 
 | ||||
| 	textarea.focus() | ||||
| 	if (old !== undefined) | ||||
| 		old.input = textarea.value | ||||
| 
 | ||||
| 	if (b !== undefined) | ||||
| 		textarea.value = b.input || '' | ||||
| 	else | ||||
| 		textarea.value = '' | ||||
| }) | ||||
| 
 | ||||
| rpc.addEventListener('BufferLine', event => { | ||||
| @ -204,7 +226,56 @@ for (let i = 0; i < 24; i++) { | ||||
| 	palette[232 + i] = `#${g}${g}${g}` | ||||
| } | ||||
| 
 | ||||
| function applyColor(fg, bg, inverse) { | ||||
| // ---- UI ---------------------------------------------------------------------
 | ||||
| 
 | ||||
| let Toolbar = { | ||||
| 	toggleAutoscroll: () => { | ||||
| 		bufferAutoscroll = !bufferAutoscroll | ||||
| 	}, | ||||
| 
 | ||||
| 	toggleLog: () => { | ||||
| 		if (bufferLog) { | ||||
| 			bufferLog = undefined | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		rpc.send({ | ||||
| 			command: 'BufferLog', | ||||
| 			bufferName: bufferCurrent, | ||||
| 		}).then(resp => { | ||||
| 			bufferLog = rpc.base64decode(resp.log) | ||||
| 			m.redraw() | ||||
| 		}) | ||||
| 	}, | ||||
| 
 | ||||
| 	view: vnode => { | ||||
| 		return m('.toolbar', {}, [ | ||||
| 			m('button', {onclick: Toolbar.toggleAutoscroll}, | ||||
| 				bufferAutoscroll ? 'Pause autoscroll' : 'Unpause autoscroll'), | ||||
| 			m('button', {onclick: Toolbar.toggleLog}, | ||||
| 				bufferLog === undefined ? 'Show log' : 'Hide log'), | ||||
| 		]) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| let BufferList = { | ||||
| 	activate: name => { | ||||
| 		rpc.send({command: 'BufferActivate', bufferName: name}) | ||||
| 	}, | ||||
| 
 | ||||
| 	view: vnode => { | ||||
| 		let items = Array.from(buffers, ([name, b]) => { | ||||
| 			let attrs = {onclick: event => BufferList.activate(name)} | ||||
| 			if (name == bufferCurrent) | ||||
| 				attrs.class = 'active' | ||||
| 			return m('.item', attrs, name) | ||||
| 		}) | ||||
| 		return m('.list', {}, items) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| let Content = { | ||||
| 	applyColor: (fg, bg, inverse) => { | ||||
| 		if (inverse) | ||||
| 			[fg, bg] = [bg >= 0 ? bg : 15, fg >= 0 ? fg : 0] | ||||
| 
 | ||||
| @ -215,35 +286,16 @@ function applyColor(fg, bg, inverse) { | ||||
| 			style.backgroundColor = palette[bg] | ||||
| 		if (style) | ||||
| 			return style | ||||
| } | ||||
| 
 | ||||
| // ---- UI ---------------------------------------------------------------------
 | ||||
| 
 | ||||
| let BufferList = { | ||||
| 	view: vnode => { | ||||
| 		let items = [] | ||||
| 		buffers.forEach((b, name) => { | ||||
| 			let attrs = { | ||||
| 				onclick: event => { | ||||
| 					rpc.send({command: 'BufferActivate', bufferName: name}) | ||||
| 	}, | ||||
| 			} | ||||
| 			if (name == bufferCurrent) | ||||
| 				attrs.class = 'active' | ||||
| 			items.push(m('.item', attrs, name)) | ||||
| 		}) | ||||
| 		return m('.list', {}, items) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| function linkify(text, attrs, a) { | ||||
| 	linkify: (text, attrs) => { | ||||
| 		let re = new RegExp([ | ||||
| 			/https?:\/\//, | ||||
| 			/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/, | ||||
| 			/[^\[\](){}<>"'\s,.:]/, | ||||
| 		].map(r => r.source).join(''), 'g') | ||||
| 
 | ||||
| 	let end = 0, match | ||||
| 		let a = [], end = 0, match | ||||
| 		while ((match = re.exec(text)) !== null) { | ||||
| 			if (end < match.index) | ||||
| 				a.push(m('span', attrs, text.substring(end, match.index))) | ||||
| @ -252,19 +304,19 @@ function linkify(text, attrs, a) { | ||||
| 		} | ||||
| 		if (end < text.length) | ||||
| 			a.push(m('span', attrs, text.substring(end))) | ||||
| } | ||||
| 		return a | ||||
| 	}, | ||||
| 
 | ||||
| let Content = { | ||||
| 	view: vnode => { | ||||
| 		let line = vnode.children[0] | ||||
| 		let content = [] | ||||
| 		let mark = undefined | ||||
| 		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 | ||||
| 		case 'Action': content.push(m('span.mark.action', {}, '✶')); break | ||||
| 		case 'Indent': mark = m('span.mark',        {}, '');  break | ||||
| 		case 'Status': mark = m('span.mark',        {}, '–'); break | ||||
| 		case 'Error':  mark = m('span.mark.error',  {}, '⚠'); break | ||||
| 		case 'Join':   mark = m('span.mark.join',   {}, '→'); break | ||||
| 		case 'Part':   mark = m('span.mark.part',   {}, '←'); break | ||||
| 		case 'Action': mark = m('span.mark.action', {}, '✶'); break | ||||
| 		} | ||||
| 
 | ||||
| 		let classes = new Set() | ||||
| @ -275,14 +327,13 @@ let Content = { | ||||
| 				classes.add(c) | ||||
| 		} | ||||
| 		let fg = -1, bg = -1, inverse = false | ||||
| 		line.items.forEach(item => { | ||||
| 		return m('.content', {}, [mark, line.items.flatMap(item => { | ||||
| 			switch (item.kind) { | ||||
| 			case 'Text': | ||||
| 				linkify(item.text, { | ||||
| 				return Content.linkify(item.text, { | ||||
| 					class: Array.from(classes.keys()).join(' '), | ||||
| 					style: applyColor(fg, bg, inverse), | ||||
| 				}, content) | ||||
| 				break | ||||
| 					style: Content.applyColor(fg, bg, inverse), | ||||
| 				}) | ||||
| 			case 'Reset': | ||||
| 				classes.clear() | ||||
| 				fg = bg = -1 | ||||
| @ -297,25 +348,30 @@ let Content = { | ||||
| 			case 'FlipInverse': | ||||
| 				inverse = !inverse | ||||
| 				break | ||||
| 			case 'FlipBold':       flip('b'); break | ||||
| 			case 'FlipItalic':     flip('i'); break | ||||
| 			case 'FlipUnderline':  flip('u'); break | ||||
| 			case 'FlipCrossedOut': flip('s'); break | ||||
| 			case 'FlipMonospace':  flip('m'); break | ||||
| 			case 'FlipBold': | ||||
| 				flip('b') | ||||
| 				break | ||||
| 			case 'FlipItalic': | ||||
| 				flip('i') | ||||
| 				break | ||||
| 			case 'FlipUnderline': | ||||
| 				flip('u') | ||||
| 				break | ||||
| 			case 'FlipCrossedOut': | ||||
| 				flip('s') | ||||
| 				break | ||||
| 			case 'FlipMonospace': | ||||
| 				flip('m') | ||||
| 				break | ||||
| 			} | ||||
| 		}) | ||||
| 		return m('.content', {}, content) | ||||
| 		})]) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| let Buffer = { | ||||
| 	oncreate: vnode => { | ||||
| 		if (vnode.dom === undefined) | ||||
| 			return | ||||
| 
 | ||||
| 		let el = vnode.dom.children[1] | ||||
| 		if (el !== null) | ||||
| 			el.scrollTop = el.scrollHeight | ||||
| 		if (vnode.dom !== undefined && bufferAutoscroll) | ||||
| 			vnode.dom.scrollTop = vnode.dom.scrollHeight | ||||
| 	}, | ||||
| 
 | ||||
| 	onupdate: vnode => { | ||||
| @ -340,62 +396,84 @@ let Buffer = { | ||||
| 			lines.push(m('.time', {}, date.toLocaleTimeString())) | ||||
| 			lines.push(m(Content, {}, line)) | ||||
| 		}) | ||||
| 		return m('.buffer', {}, lines) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| let Log = { | ||||
| 	oncreate: vnode => { | ||||
| 		if (vnode.dom !== undefined) | ||||
| 			vnode.dom.scrollTop = vnode.dom.scrollHeight | ||||
| 	}, | ||||
| 
 | ||||
| 	view: vnode => { | ||||
| 		return m(".log", {}, bufferLog) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| let BufferContainer = { | ||||
| 	view: vnode => { | ||||
| 		return m('.buffer-container', {}, [ | ||||
| 			m('.filler'), | ||||
| 			m('.buffer', {}, lines), | ||||
| 			bufferLog !== undefined ? m(Log) : m(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: | ||||
| let Input = { | ||||
| 	complete: textarea => { | ||||
| 		if (textarea.selectionStart !== textarea.selectionEnd) | ||||
| 			return | ||||
| 			return false | ||||
| 
 | ||||
| 		rpc.send({ | ||||
| 			command: 'BufferComplete', | ||||
| 			bufferName: bufferCurrent, | ||||
| 			text: textarea.value, | ||||
| 			position: textarea.selectionEnd, | ||||
| 		}).then(response => { | ||||
| 		}).then(resp => { | ||||
| 			// 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) | ||||
| 			if (resp.completions.length) | ||||
| 				textarea.setRangeText(resp.completions[0], | ||||
| 					resp.start, textarea.selectionEnd, 'end') | ||||
| 			if (resp.completions.length === 1) | ||||
| 				textarea.setRangeText(' ', | ||||
| 					textarea.selectionStart, textarea.selectionEnd, 'end') | ||||
| 		}) | ||||
| 		break; | ||||
| 	case 13: | ||||
| 		return true | ||||
| 	}, | ||||
| 
 | ||||
| 	submit: textarea => { | ||||
| 		rpc.send({ | ||||
| 			command: 'BufferInput', | ||||
| 			bufferName: bufferCurrent, | ||||
| 			text: textarea.value, | ||||
| 		}) | ||||
| 		textarea.value = '' | ||||
| 		break; | ||||
| 	default: | ||||
| 		return | ||||
| 		return true | ||||
| 	}, | ||||
| 
 | ||||
| 	onKeyDown: event => { | ||||
| 		// TODO: And perhaps on other actions, too.
 | ||||
| 		rpc.send({command: 'Active'}) | ||||
| 
 | ||||
| 		// TODO: Cancel any current autocomplete.
 | ||||
| 
 | ||||
| 		let textarea = event.currentTarget | ||||
| 		let handled = false | ||||
| 		switch (event.keyCode) { | ||||
| 		case 9: | ||||
| 			handled = Input.complete(textarea) | ||||
| 			break | ||||
| 		case 13: | ||||
| 			handled = Input.submit(textarea) | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		if (handled) | ||||
| 			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#input', { | ||||
| 			rows: 1, | ||||
| 			onkeydown: onKeyDown, | ||||
| 		}) | ||||
| 		return m('textarea#input', {rows: 1, onkeydown: Input.onKeyDown}) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| @ -408,8 +486,8 @@ let Main = { | ||||
| 			state = "Disconnected" | ||||
| 
 | ||||
| 		return m('.xP', {}, [ | ||||
| 			m('.title', {}, `xP (${state})`), | ||||
| 			m('.middle', {}, [m(BufferList), m(Buffer)]), | ||||
| 			m('.title', {}, [`xP (${state})`, m(Toolbar)]), | ||||
| 			m('.middle', {}, [m(BufferList), m(BufferContainer)]), | ||||
| 			m('.status', {}, bufferCurrent), | ||||
| 			m(Input), | ||||
| 		]) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user