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