Compare commits
	
		
			5 Commits
		
	
	
		
			d7b0b447b7
			...
			8cd94b30f6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8cd94b30f6 | |||
| 2d30b6d115 | |||
| cf14cb8122 | |||
| 31e9c6d2d5 | |||
| d2af6cf64c | 
| @ -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 "" | ||||
|  | ||||
							
								
								
									
										4
									
								
								xC-proto
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								xC-proto
									
									
									
									
									
								
							| @ -71,7 +71,7 @@ struct EventMessage { | ||||
| 			PART, | ||||
| 			ACTION, | ||||
| 		} rendition; | ||||
| 		// Unix timestamp in seconds. | ||||
| 		// Unix timestamp in milliseconds. | ||||
| 		u64 when; | ||||
| 		// Broken-up text, with in-band formatting. | ||||
| 		union ItemData switch (enum Item { | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										181
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								xC.c
									
									
									
									
									
								
							| @ -1032,7 +1032,7 @@ input_el__restore (struct input_el *self) | ||||
| static void | ||||
| input_el__start_over (struct input_el *self) | ||||
| { | ||||
| 	wchar_t x[] = { L'g' & 31, 0 }; | ||||
| 	wchar_t x[] = { L'c' & 31, 0 }; | ||||
| 	el_wpush (self->editline, x); | ||||
| 
 | ||||
| 	int dummy_count = 0; | ||||
| @ -3105,7 +3105,7 @@ relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer, | ||||
| 	e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT); | ||||
| 	e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT); | ||||
| 	e->rendition = 1 + line->r; | ||||
| 	e->when = line->when; | ||||
| 	e->when = line->when * 1000; | ||||
| 
 | ||||
| 	size_t len = 0; | ||||
| 	for (size_t i = 0; line->items[i].type; i++) | ||||
| @ -13271,12 +13271,6 @@ process_input (struct app_context *ctx, char *user_input) | ||||
| // The amount of crap that goes into this is truly insane.
 | ||||
| // It's mostly because of Editline's total ignorance of this task.
 | ||||
| 
 | ||||
| static void | ||||
| completion_init (struct completion *self) | ||||
| { | ||||
| 	memset (self, 0, sizeof *self); | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| completion_free (struct completion *self) | ||||
| { | ||||
| @ -13295,13 +13289,13 @@ completion_add_word (struct completion *self, size_t start, size_t end) | ||||
| 	self->words[self->words_len++] = (struct completion_word) { start, end }; | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| completion_parse (struct completion *self, const char *line, size_t len) | ||||
| static struct completion | ||||
| completion_make (const char *line, size_t len) | ||||
| { | ||||
| 	self->line = xstrndup (line, len); | ||||
| 	struct completion self = { .line = xstrndup (line, len) }; | ||||
| 
 | ||||
| 	// The first and the last word may be empty
 | ||||
| 	const char *s = self->line; | ||||
| 	const char *s = self.line; | ||||
| 	while (true) | ||||
| 	{ | ||||
| 		const char *start = s; | ||||
| @ -13309,10 +13303,11 @@ completion_parse (struct completion *self, const char *line, size_t len) | ||||
| 		const char *end = start + word_len; | ||||
| 		s = end + strspn (end, WORD_BREAKING_CHARS); | ||||
| 
 | ||||
| 		completion_add_word (self, start - self->line, end - self->line); | ||||
| 		completion_add_word (&self, start - self.line, end - self.line); | ||||
| 		if (s == end) | ||||
| 			break; | ||||
| 	} | ||||
| 	return self; | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| @ -13486,14 +13481,13 @@ complete_set (struct app_context *ctx, struct completion *data, | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| complete_topic (struct app_context *ctx, struct completion *data, | ||||
| complete_topic (struct buffer *buffer, struct completion *data, | ||||
| 	const char *word, struct strv *output) | ||||
| { | ||||
| 	(void) data; | ||||
| 
 | ||||
| 	// TODO: make it work in other server-related buffers, too, i.e. when we're
 | ||||
| 	//   completing the third word and the second word is a known channel name
 | ||||
| 	struct buffer *buffer = ctx->current_buffer; | ||||
| 	if (buffer->type != BUFFER_CHANNEL) | ||||
| 		return; | ||||
| 
 | ||||
| @ -13509,10 +13503,9 @@ complete_topic (struct app_context *ctx, struct completion *data, | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| complete_nicknames (struct app_context *ctx, struct completion *data, | ||||
| complete_nicknames (struct buffer *buffer, struct completion *data, | ||||
| 	const char *word, struct strv *output) | ||||
| { | ||||
| 	struct buffer *buffer = ctx->current_buffer; | ||||
| 	if (buffer->type == BUFFER_SERVER) | ||||
| 	{ | ||||
| 		struct user *self_user = buffer->server->irc_user; | ||||
| @ -13534,9 +13527,9 @@ complete_nicknames (struct app_context *ctx, struct completion *data, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| static char ** | ||||
| complete_word (struct app_context *ctx, struct completion *data, | ||||
| 	const char *word) | ||||
| static struct strv | ||||
| complete_word (struct app_context *ctx, struct buffer *buffer, | ||||
| 	struct completion *data, const char *word) | ||||
| { | ||||
| 	char *initial = completion_word (data, 0); | ||||
| 
 | ||||
| @ -13555,11 +13548,11 @@ complete_word (struct app_context *ctx, struct completion *data, | ||||
| 	} | ||||
| 	else if (data->location == 1 && !strcmp (initial, "/topic")) | ||||
| 	{ | ||||
| 		complete_topic     (ctx, data, word, &words); | ||||
| 		complete_nicknames (ctx, data, word, &words); | ||||
| 		complete_topic     (buffer, data, word, &words); | ||||
| 		complete_nicknames (buffer, data, word, &words); | ||||
| 	} | ||||
| 	else | ||||
| 		complete_nicknames (ctx, data, word, &words); | ||||
| 		complete_nicknames (buffer, data, word, &words); | ||||
| 
 | ||||
| 	cstr_set (&initial, NULL); | ||||
| 	LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks) | ||||
| @ -13568,17 +13561,12 @@ complete_word (struct app_context *ctx, struct completion *data, | ||||
| 		hook->complete (hook, data, word, &words); | ||||
| 	} | ||||
| 
 | ||||
| 	if (words.len == 1) | ||||
| 	{ | ||||
| 		// Nothing matched
 | ||||
| 		strv_free (&words); | ||||
| 		return NULL; | ||||
| 	} | ||||
| 
 | ||||
| 	if (words.len == 2) | ||||
| 	if (words.len <= 2) | ||||
| 	{ | ||||
| 		// When nothing matches, this copies the sentinel value
 | ||||
| 		words.vector[0] = words.vector[1]; | ||||
| 		words.vector[1] = NULL; | ||||
| 		words.len--; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| @ -13589,7 +13577,7 @@ complete_word (struct app_context *ctx, struct completion *data, | ||||
| 		else | ||||
| 			words.vector[0] = xstrndup (words.vector[1], prefix); | ||||
| 	} | ||||
| 	return words.vector; | ||||
| 	return words; | ||||
| } | ||||
| 
 | ||||
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||
| @ -13655,26 +13643,26 @@ locale_to_utf8 (struct app_context *ctx, const char *locale, | ||||
| 	return str_steal (&utf8); | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| utf8_vector_to_locale (struct app_context *ctx, char **vector) | ||||
| { | ||||
| 	for (; *vector; vector++) | ||||
| 	{ | ||||
| 		char *converted = iconv_xstrdup | ||||
| 			(ctx->term_from_utf8, *vector, -1, NULL); | ||||
| 		if (!soft_assert (converted)) | ||||
| 			converted = xstrdup (""); | ||||
| 
 | ||||
| 		cstr_set (vector, converted); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||
| 
 | ||||
| static struct strv | ||||
| make_completions (struct app_context *ctx, struct buffer *buffer, | ||||
| 	const char *line_utf8, size_t start, size_t end) | ||||
| { | ||||
| 	struct completion comp = completion_make (line_utf8, strlen (line_utf8)); | ||||
| 	completion_locate (&comp, start); | ||||
| 	char *word = xstrndup (line_utf8 + start, end - start); | ||||
| 	struct strv completions = complete_word (ctx, buffer, &comp, word); | ||||
| 	free (word); | ||||
| 	completion_free (&comp); | ||||
| 	return completions; | ||||
| } | ||||
| 
 | ||||
| /// Takes a line in locale-specific encoding and position of a word to complete,
 | ||||
| /// returns a vector of matches in locale-specific encoding.
 | ||||
| static char ** | ||||
| make_completions (struct app_context *ctx, char *line, int start, int end) | ||||
| make_input_completions | ||||
| 	(struct app_context *ctx, const char *line, int start, int end) | ||||
| { | ||||
| 	int *fixes[] = { &start, &end }; | ||||
| 	char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes)); | ||||
| @ -13683,20 +13671,23 @@ make_completions (struct app_context *ctx, char *line, int start, int end) | ||||
| 
 | ||||
| 	hard_assert (start >= 0 && end >= 0 && start <= end); | ||||
| 
 | ||||
| 	struct completion c; | ||||
| 	completion_init (&c); | ||||
| 	completion_parse (&c, line, strlen (line)); | ||||
| 	completion_locate (&c, start); | ||||
| 	char *word = xstrndup (line + start, end - start); | ||||
| 	char **completions = complete_word (ctx, &c, word); | ||||
| 	free (word); | ||||
| 	completion_free (&c); | ||||
| 
 | ||||
| 	if (completions) | ||||
| 		utf8_vector_to_locale (ctx, completions); | ||||
| 
 | ||||
| 	struct strv completions = | ||||
| 		make_completions (ctx, ctx->current_buffer, line_utf8, start, end); | ||||
| 	free (line_utf8); | ||||
| 	return completions; | ||||
| 	if (!completions.len) | ||||
| 	{ | ||||
| 		strv_free (&completions); | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	for (size_t i = 0; i < completions.len; i++) | ||||
| 	{ | ||||
| 		char *converted = iconv_xstrdup | ||||
| 			(ctx->term_from_utf8, completions.vector[i], -1, NULL); | ||||
| 		if (!soft_assert (converted)) | ||||
| 			converted = xstrdup ("?"); | ||||
| 		cstr_set (&completions.vector[i], converted); | ||||
| 	} | ||||
| 	return completions.vector; | ||||
| } | ||||
| 
 | ||||
| // --- Common code for user actions --------------------------------------------
 | ||||
| @ -14381,7 +14372,7 @@ app_readline_completion (const char *text, int start, int end) | ||||
| 	// Don't iterate over filenames and stuff
 | ||||
| 	rl_attempted_completion_over = true; | ||||
| 
 | ||||
| 	return make_completions (g_ctx, rl_line_buffer, start, end); | ||||
| 	return make_input_completions (g_ctx, rl_line_buffer, start, end); | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| @ -14425,8 +14416,6 @@ static unsigned char | ||||
| on_editline_complete (EditLine *editline, int key) | ||||
| { | ||||
| 	(void) key; | ||||
| 	(void) editline; | ||||
| 
 | ||||
| 	struct app_context *ctx = g_ctx; | ||||
| 
 | ||||
| 	// First prepare what Readline would have normally done for us...
 | ||||
| @ -14440,7 +14429,7 @@ on_editline_complete (EditLine *editline, int key) | ||||
| 	while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1])) | ||||
| 		el_start--; | ||||
| 
 | ||||
| 	char **completions = make_completions (ctx, copy, el_start, el_end); | ||||
| 	char **completions = make_input_completions (ctx, copy, el_start, el_end); | ||||
| 
 | ||||
| 	// XXX: possibly incorrect wrt. shift state encodings
 | ||||
| 	copy[el_end] = '\0'; | ||||
| @ -14538,12 +14527,6 @@ app_editline_init (struct input_el *self) | ||||
| 	// Just what are you doing?
 | ||||
| 	CALL_ (input, bind_control, 'u', "vi-kill-line-prev"); | ||||
| 
 | ||||
| 	// See input_el__redisplay(), functionally important
 | ||||
| 	CALL_ (input, bind_control, 'q', "ed-redisplay"); | ||||
| 	// This is what buffered el_wgets() does, functionally important;
 | ||||
| 	// perhaps it could be bound somewhere more appropriate
 | ||||
| 	CALL_ (input, bind_control, 'g', "ed-start-over"); | ||||
| 
 | ||||
| 	// We need to hide the prompt and input first
 | ||||
| 	CALL_ (input, bind, "\r", "send-line"); | ||||
| 	CALL_ (input, bind, "\n", "send-line"); | ||||
| @ -14552,6 +14535,11 @@ app_editline_init (struct input_el *self) | ||||
| 
 | ||||
| 	// Source the user's defaults file
 | ||||
| 	el_source (self->editline, NULL); | ||||
| 
 | ||||
| 	// See input_el__redisplay(), functionally important
 | ||||
| 	CALL_ (input, bind_control, 'q', "ed-redisplay"); | ||||
| 	// This is what buffered el_wgets() does, functionally important
 | ||||
| 	CALL_ (input, bind_control, 'c', "ed-start-over"); | ||||
| } | ||||
| 
 | ||||
| #endif // HAVE_EDITLINE
 | ||||
| @ -15178,12 +15166,56 @@ client_message_buffer_name (const struct relay_command_message *m) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| client_process_buffer_complete (struct client *c, uint32_t seq, | ||||
| 	struct buffer *buffer, struct relay_command_data_buffer_complete *req) | ||||
| { | ||||
| 	struct str *line = &req->text; | ||||
| 	uint32_t end = req->position; | ||||
| 	if (line->len < end || line->len != strlen (line->str)) | ||||
| 	{ | ||||
| 		relay_prepare_error (c->ctx, seq, "Invalid arguments"); | ||||
| 		goto out; | ||||
| 	} | ||||
| 
 | ||||
| 	uint32_t start = end; | ||||
| 	while (start && !strchr (WORD_BREAKING_CHARS, line->str[start - 1])) | ||||
| 		start--; | ||||
| 
 | ||||
| 	struct strv completions = | ||||
| 		make_completions (c->ctx, buffer, line->str, start, end); | ||||
| 	if (completions.len > UINT32_MAX) | ||||
| 	{ | ||||
| 		relay_prepare_error (c->ctx, seq, "Internal error"); | ||||
| 		goto out_internal; | ||||
| 	} | ||||
| 
 | ||||
| 	struct relay_event_data_response *e = | ||||
| 		&relay_prepare (c->ctx)->data.response; | ||||
| 	e->event = RELAY_EVENT_RESPONSE; | ||||
| 	e->command_seq = seq; | ||||
| 	e->data.command = RELAY_COMMAND_BUFFER_COMPLETE; | ||||
| 
 | ||||
| 	struct relay_response_data_buffer_complete *resp = | ||||
| 		&e->data.buffer_complete; | ||||
| 	resp->start = start; | ||||
| 	resp->completions_len = completions.len; | ||||
| 	resp->completions = xcalloc (completions.len, sizeof *resp->completions); | ||||
| 	for (size_t i = 0; i < completions.len; i++) | ||||
| 		resp->completions[i] = str_from_cstr (completions.vector[i]); | ||||
| 
 | ||||
| out_internal: | ||||
| 	strv_free (&completions); | ||||
| out: | ||||
| 	relay_send (c); | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| client_process_buffer_log | ||||
| 	(struct client *c, uint32_t seq, struct buffer *buffer) | ||||
| { | ||||
| 	struct relay_event_message *m = relay_prepare (c->ctx); | ||||
| 	struct relay_event_data_response *e = &m->data.response; | ||||
| 	struct relay_event_data_response *e = | ||||
| 		&relay_prepare (c->ctx)->data.response; | ||||
| 	e->event = RELAY_EVENT_RESPONSE; | ||||
| 	e->command_seq = seq; | ||||
| 	e->data.command = RELAY_COMMAND_BUFFER_LOG; | ||||
| @ -15254,9 +15286,8 @@ client_process_message (struct client *c, | ||||
| 		reset_autoaway (c->ctx); | ||||
| 		break; | ||||
| 	case RELAY_COMMAND_BUFFER_COMPLETE: | ||||
| 		// TODO: Run the completion machinery.
 | ||||
| 		relay_prepare_error (c->ctx, m->command_seq, "Not implemented"); | ||||
| 		relay_send (c); | ||||
| 		client_process_buffer_complete (c, m->command_seq, buffer, | ||||
| 			&m->data.buffer_complete); | ||||
| 		break; | ||||
| 	case RELAY_COMMAND_BUFFER_INPUT: | ||||
| 		(void) process_input_utf8 (c->ctx, | ||||
|  | ||||
| @ -86,19 +86,19 @@ body { | ||||
| 	padding: .1rem .3rem; | ||||
| 	white-space: pre-wrap; | ||||
| } | ||||
| .content span.b { | ||||
| .content .b { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| .content span.i { | ||||
| .content .i { | ||||
| 	font-style: italic; | ||||
| } | ||||
| .content span.u { | ||||
| .content .u { | ||||
| 	text-decoration: underline; | ||||
| } | ||||
| .content span.s { | ||||
| .content .s { | ||||
| 	text-decoration: line-through; | ||||
| } | ||||
| .content span.m { | ||||
| .content .m { | ||||
| 	font-family: monospace; | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										318
									
								
								xP/public/xP.js
									
									
									
									
									
								
							
							
						
						
									
										318
									
								
								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) | ||||
| @ -115,6 +222,24 @@ let BufferList = { | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| 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 = { | ||||
| 	view: vnode => { | ||||
| 		let line = vnode.children[0] | ||||
| @ -139,11 +264,10 @@ let Content = { | ||||
| 		line.items.forEach(item => { | ||||
| 			switch (item.kind) { | ||||
| 			case 'Text': | ||||
| 				// TODO: Detect and transform links.
 | ||||
| 				content.push(m('span', { | ||||
| 				linkify(item.text, { | ||||
| 					class: Array.from(classes.keys()).join(' '), | ||||
| 					style: applyColor(fg, bg, inverse), | ||||
| 				}, item.text)) | ||||
| 				}, content) | ||||
| 				break | ||||
| 			case 'Reset': | ||||
| 				classes.clear() | ||||
| @ -179,7 +303,7 @@ let Buffer = { | ||||
| 
 | ||||
| 		let lastDateMark = undefined | ||||
| 		b.lines.forEach(line => { | ||||
| 			let date = new Date(line.when * 1000) | ||||
| 			let date = new Date(line.when) | ||||
| 			let dateMark = date.toLocaleDateString() | ||||
| 			if (dateMark !== lastDateMark) { | ||||
| 				lines.push(m('.date', {}, dateMark)) | ||||
| @ -196,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, | ||||
| 		}) | ||||
| 	}, | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user