Compare commits
	
		
			No commits in common. "8cd94b30f67f8eacf853fbc77bd60fb57a8262dc" and "d7b0b447b7e254d4af37f09acfa258956d24a7e0" have entirely different histories.
		
	
	
		
			8cd94b30f6
			...
			d7b0b447b7
		
	
		
| @ -382,8 +382,7 @@ function codegen_union(name, cg,    gotype, tagfield, tagvar) { | |||||||
| 	print "}" | 	print "}" | ||||||
| 	print "" | 	print "" | ||||||
| 
 | 
 | ||||||
| 	# This cannot be a pointer method, it wouldn't work recursively. | 	print "func (u *" gotype ") MarshalJSON() ([]byte, error) {" | ||||||
| 	print "func (u " gotype ") MarshalJSON() ([]byte, error) {" |  | ||||||
| 	print "\treturn json.Marshal(u.Interface)" | 	print "\treturn json.Marshal(u.Interface)" | ||||||
| 	print "}" | 	print "}" | ||||||
| 	print "" | 	print "" | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								xC-proto
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								xC-proto
									
									
									
									
									
								
							| @ -71,7 +71,7 @@ struct EventMessage { | |||||||
| 			PART, | 			PART, | ||||||
| 			ACTION, | 			ACTION, | ||||||
| 		} rendition; | 		} rendition; | ||||||
| 		// Unix timestamp in milliseconds. | 		// Unix timestamp in seconds. | ||||||
| 		u64 when; | 		u64 when; | ||||||
| 		// Broken-up text, with in-band formatting. | 		// Broken-up text, with in-band formatting. | ||||||
| 		union ItemData switch (enum Item { | 		union ItemData switch (enum Item { | ||||||
| @ -104,8 +104,6 @@ struct EventMessage { | |||||||
| 		} items<>; | 		} items<>; | ||||||
| 	case BUFFER_CLEAR: | 	case BUFFER_CLEAR: | ||||||
| 		string buffer_name; | 		string buffer_name; | ||||||
| 
 |  | ||||||
| 	// Restriction: command_seq is strictly increasing, across both of these. |  | ||||||
| 	case ERROR: | 	case ERROR: | ||||||
| 		u32 command_seq; | 		u32 command_seq; | ||||||
| 		string error; | 		string error; | ||||||
|  | |||||||
							
								
								
									
										179
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								xC.c
									
									
									
									
									
								
							| @ -1032,7 +1032,7 @@ input_el__restore (struct input_el *self) | |||||||
| static void | static void | ||||||
| input_el__start_over (struct input_el *self) | input_el__start_over (struct input_el *self) | ||||||
| { | { | ||||||
| 	wchar_t x[] = { L'c' & 31, 0 }; | 	wchar_t x[] = { L'g' & 31, 0 }; | ||||||
| 	el_wpush (self->editline, x); | 	el_wpush (self->editline, x); | ||||||
| 
 | 
 | ||||||
| 	int dummy_count = 0; | 	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_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT); | ||||||
| 	e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT); | 	e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT); | ||||||
| 	e->rendition = 1 + line->r; | 	e->rendition = 1 + line->r; | ||||||
| 	e->when = line->when * 1000; | 	e->when = line->when; | ||||||
| 
 | 
 | ||||||
| 	size_t len = 0; | 	size_t len = 0; | ||||||
| 	for (size_t i = 0; line->items[i].type; i++) | 	for (size_t i = 0; line->items[i].type; i++) | ||||||
| @ -13271,6 +13271,12 @@ process_input (struct app_context *ctx, char *user_input) | |||||||
| // The amount of crap that goes into this is truly insane.
 | // The amount of crap that goes into this is truly insane.
 | ||||||
| // It's mostly because of Editline's total ignorance of this task.
 | // 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 | static void | ||||||
| completion_free (struct completion *self) | completion_free (struct completion *self) | ||||||
| { | { | ||||||
| @ -13289,13 +13295,13 @@ completion_add_word (struct completion *self, size_t start, size_t end) | |||||||
| 	self->words[self->words_len++] = (struct completion_word) { start, end }; | 	self->words[self->words_len++] = (struct completion_word) { start, end }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static struct completion | static void | ||||||
| completion_make (const char *line, size_t len) | completion_parse (struct completion *self, const char *line, size_t len) | ||||||
| { | { | ||||||
| 	struct completion self = { .line = xstrndup (line, len) }; | 	self->line = xstrndup (line, len); | ||||||
| 
 | 
 | ||||||
| 	// The first and the last word may be empty
 | 	// The first and the last word may be empty
 | ||||||
| 	const char *s = self.line; | 	const char *s = self->line; | ||||||
| 	while (true) | 	while (true) | ||||||
| 	{ | 	{ | ||||||
| 		const char *start = s; | 		const char *start = s; | ||||||
| @ -13303,11 +13309,10 @@ completion_make (const char *line, size_t len) | |||||||
| 		const char *end = start + word_len; | 		const char *end = start + word_len; | ||||||
| 		s = end + strspn (end, WORD_BREAKING_CHARS); | 		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) | 		if (s == end) | ||||||
| 			break; | 			break; | ||||||
| 	} | 	} | ||||||
| 	return self; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| @ -13481,13 +13486,14 @@ complete_set (struct app_context *ctx, struct completion *data, | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| complete_topic (struct buffer *buffer, struct completion *data, | complete_topic (struct app_context *ctx, struct completion *data, | ||||||
| 	const char *word, struct strv *output) | 	const char *word, struct strv *output) | ||||||
| { | { | ||||||
| 	(void) data; | 	(void) data; | ||||||
| 
 | 
 | ||||||
| 	// TODO: make it work in other server-related buffers, too, i.e. when we're
 | 	// 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
 | 	//   completing the third word and the second word is a known channel name
 | ||||||
|  | 	struct buffer *buffer = ctx->current_buffer; | ||||||
| 	if (buffer->type != BUFFER_CHANNEL) | 	if (buffer->type != BUFFER_CHANNEL) | ||||||
| 		return; | 		return; | ||||||
| 
 | 
 | ||||||
| @ -13503,9 +13509,10 @@ complete_topic (struct buffer *buffer, struct completion *data, | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| complete_nicknames (struct buffer *buffer, struct completion *data, | complete_nicknames (struct app_context *ctx, struct completion *data, | ||||||
| 	const char *word, struct strv *output) | 	const char *word, struct strv *output) | ||||||
| { | { | ||||||
|  | 	struct buffer *buffer = ctx->current_buffer; | ||||||
| 	if (buffer->type == BUFFER_SERVER) | 	if (buffer->type == BUFFER_SERVER) | ||||||
| 	{ | 	{ | ||||||
| 		struct user *self_user = buffer->server->irc_user; | 		struct user *self_user = buffer->server->irc_user; | ||||||
| @ -13527,9 +13534,9 @@ complete_nicknames (struct buffer *buffer, struct completion *data, | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static struct strv | static char ** | ||||||
| complete_word (struct app_context *ctx, struct buffer *buffer, | complete_word (struct app_context *ctx, struct completion *data, | ||||||
| 	struct completion *data, const char *word) | 	const char *word) | ||||||
| { | { | ||||||
| 	char *initial = completion_word (data, 0); | 	char *initial = completion_word (data, 0); | ||||||
| 
 | 
 | ||||||
| @ -13548,11 +13555,11 @@ complete_word (struct app_context *ctx, struct buffer *buffer, | |||||||
| 	} | 	} | ||||||
| 	else if (data->location == 1 && !strcmp (initial, "/topic")) | 	else if (data->location == 1 && !strcmp (initial, "/topic")) | ||||||
| 	{ | 	{ | ||||||
| 		complete_topic     (buffer, data, word, &words); | 		complete_topic     (ctx, data, word, &words); | ||||||
| 		complete_nicknames (buffer, data, word, &words); | 		complete_nicknames (ctx, data, word, &words); | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| 		complete_nicknames (buffer, data, word, &words); | 		complete_nicknames (ctx, data, word, &words); | ||||||
| 
 | 
 | ||||||
| 	cstr_set (&initial, NULL); | 	cstr_set (&initial, NULL); | ||||||
| 	LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks) | 	LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks) | ||||||
| @ -13561,12 +13568,17 @@ complete_word (struct app_context *ctx, struct buffer *buffer, | |||||||
| 		hook->complete (hook, data, word, &words); | 		hook->complete (hook, data, word, &words); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (words.len <= 2) | 	if (words.len == 1) | ||||||
|  | 	{ | ||||||
|  | 		// Nothing matched
 | ||||||
|  | 		strv_free (&words); | ||||||
|  | 		return NULL; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (words.len == 2) | ||||||
| 	{ | 	{ | ||||||
| 		// When nothing matches, this copies the sentinel value
 |  | ||||||
| 		words.vector[0] = words.vector[1]; | 		words.vector[0] = words.vector[1]; | ||||||
| 		words.vector[1] = NULL; | 		words.vector[1] = NULL; | ||||||
| 		words.len--; |  | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| 	{ | 	{ | ||||||
| @ -13577,7 +13589,7 @@ complete_word (struct app_context *ctx, struct buffer *buffer, | |||||||
| 		else | 		else | ||||||
| 			words.vector[0] = xstrndup (words.vector[1], prefix); | 			words.vector[0] = xstrndup (words.vector[1], prefix); | ||||||
| 	} | 	} | ||||||
| 	return words; | 	return words.vector; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
| @ -13643,26 +13655,26 @@ locale_to_utf8 (struct app_context *ctx, const char *locale, | |||||||
| 	return str_steal (&utf8); | 	return str_steal (&utf8); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | static void | ||||||
| 
 | utf8_vector_to_locale (struct app_context *ctx, char **vector) | ||||||
| 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)); | 	for (; *vector; vector++) | ||||||
| 	completion_locate (&comp, start); | 	{ | ||||||
| 	char *word = xstrndup (line_utf8 + start, end - start); | 		char *converted = iconv_xstrdup | ||||||
| 	struct strv completions = complete_word (ctx, buffer, &comp, word); | 			(ctx->term_from_utf8, *vector, -1, NULL); | ||||||
| 	free (word); | 		if (!soft_assert (converted)) | ||||||
| 	completion_free (&comp); | 			converted = xstrdup (""); | ||||||
| 	return completions; | 
 | ||||||
|  | 		cstr_set (vector, converted); | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
|  | 
 | ||||||
| /// Takes a line in locale-specific encoding and position of a word to complete,
 | /// Takes a line in locale-specific encoding and position of a word to complete,
 | ||||||
| /// returns a vector of matches in locale-specific encoding.
 | /// returns a vector of matches in locale-specific encoding.
 | ||||||
| static char ** | static char ** | ||||||
| make_input_completions | make_completions (struct app_context *ctx, char *line, int start, int end) | ||||||
| 	(struct app_context *ctx, const char *line, int start, int end) |  | ||||||
| { | { | ||||||
| 	int *fixes[] = { &start, &end }; | 	int *fixes[] = { &start, &end }; | ||||||
| 	char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes)); | 	char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes)); | ||||||
| @ -13671,23 +13683,20 @@ make_input_completions | |||||||
| 
 | 
 | ||||||
| 	hard_assert (start >= 0 && end >= 0 && start <= end); | 	hard_assert (start >= 0 && end >= 0 && start <= end); | ||||||
| 
 | 
 | ||||||
| 	struct strv completions = | 	struct completion c; | ||||||
| 		make_completions (ctx, ctx->current_buffer, line_utf8, start, end); | 	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); | ||||||
|  | 
 | ||||||
| 	free (line_utf8); | 	free (line_utf8); | ||||||
| 	if (!completions.len) | 	return completions; | ||||||
| 	{ |  | ||||||
| 		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 --------------------------------------------
 | // --- Common code for user actions --------------------------------------------
 | ||||||
| @ -14372,7 +14381,7 @@ app_readline_completion (const char *text, int start, int end) | |||||||
| 	// Don't iterate over filenames and stuff
 | 	// Don't iterate over filenames and stuff
 | ||||||
| 	rl_attempted_completion_over = true; | 	rl_attempted_completion_over = true; | ||||||
| 
 | 
 | ||||||
| 	return make_input_completions (g_ctx, rl_line_buffer, start, end); | 	return make_completions (g_ctx, rl_line_buffer, start, end); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static int | static int | ||||||
| @ -14416,6 +14425,8 @@ static unsigned char | |||||||
| on_editline_complete (EditLine *editline, int key) | on_editline_complete (EditLine *editline, int key) | ||||||
| { | { | ||||||
| 	(void) key; | 	(void) key; | ||||||
|  | 	(void) editline; | ||||||
|  | 
 | ||||||
| 	struct app_context *ctx = g_ctx; | 	struct app_context *ctx = g_ctx; | ||||||
| 
 | 
 | ||||||
| 	// First prepare what Readline would have normally done for us...
 | 	// First prepare what Readline would have normally done for us...
 | ||||||
| @ -14429,7 +14440,7 @@ on_editline_complete (EditLine *editline, int key) | |||||||
| 	while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1])) | 	while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1])) | ||||||
| 		el_start--; | 		el_start--; | ||||||
| 
 | 
 | ||||||
| 	char **completions = make_input_completions (ctx, copy, el_start, el_end); | 	char **completions = make_completions (ctx, copy, el_start, el_end); | ||||||
| 
 | 
 | ||||||
| 	// XXX: possibly incorrect wrt. shift state encodings
 | 	// XXX: possibly incorrect wrt. shift state encodings
 | ||||||
| 	copy[el_end] = '\0'; | 	copy[el_end] = '\0'; | ||||||
| @ -14527,6 +14538,12 @@ app_editline_init (struct input_el *self) | |||||||
| 	// Just what are you doing?
 | 	// Just what are you doing?
 | ||||||
| 	CALL_ (input, bind_control, 'u', "vi-kill-line-prev"); | 	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
 | 	// We need to hide the prompt and input first
 | ||||||
| 	CALL_ (input, bind, "\r", "send-line"); | 	CALL_ (input, bind, "\r", "send-line"); | ||||||
| 	CALL_ (input, bind, "\n", "send-line"); | 	CALL_ (input, bind, "\n", "send-line"); | ||||||
| @ -14535,11 +14552,6 @@ app_editline_init (struct input_el *self) | |||||||
| 
 | 
 | ||||||
| 	// Source the user's defaults file
 | 	// Source the user's defaults file
 | ||||||
| 	el_source (self->editline, NULL); | 	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
 | #endif // HAVE_EDITLINE
 | ||||||
| @ -15166,56 +15178,12 @@ 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 | static void | ||||||
| client_process_buffer_log | client_process_buffer_log | ||||||
| 	(struct client *c, uint32_t seq, struct buffer *buffer) | 	(struct client *c, uint32_t seq, struct buffer *buffer) | ||||||
| { | { | ||||||
| 	struct relay_event_data_response *e = | 	struct relay_event_message *m = relay_prepare (c->ctx); | ||||||
| 		&relay_prepare (c->ctx)->data.response; | 	struct relay_event_data_response *e = &m->data.response; | ||||||
| 	e->event = RELAY_EVENT_RESPONSE; | 	e->event = RELAY_EVENT_RESPONSE; | ||||||
| 	e->command_seq = seq; | 	e->command_seq = seq; | ||||||
| 	e->data.command = RELAY_COMMAND_BUFFER_LOG; | 	e->data.command = RELAY_COMMAND_BUFFER_LOG; | ||||||
| @ -15286,8 +15254,9 @@ client_process_message (struct client *c, | |||||||
| 		reset_autoaway (c->ctx); | 		reset_autoaway (c->ctx); | ||||||
| 		break; | 		break; | ||||||
| 	case RELAY_COMMAND_BUFFER_COMPLETE: | 	case RELAY_COMMAND_BUFFER_COMPLETE: | ||||||
| 		client_process_buffer_complete (c, m->command_seq, buffer, | 		// TODO: Run the completion machinery.
 | ||||||
| 			&m->data.buffer_complete); | 		relay_prepare_error (c->ctx, m->command_seq, "Not implemented"); | ||||||
|  | 		relay_send (c); | ||||||
| 		break; | 		break; | ||||||
| 	case RELAY_COMMAND_BUFFER_INPUT: | 	case RELAY_COMMAND_BUFFER_INPUT: | ||||||
| 		(void) process_input_utf8 (c->ctx, | 		(void) process_input_utf8 (c->ctx, | ||||||
|  | |||||||
| @ -86,19 +86,19 @@ body { | |||||||
| 	padding: .1rem .3rem; | 	padding: .1rem .3rem; | ||||||
| 	white-space: pre-wrap; | 	white-space: pre-wrap; | ||||||
| } | } | ||||||
| .content .b { | .content span.b { | ||||||
| 	font-weight: bold; | 	font-weight: bold; | ||||||
| } | } | ||||||
| .content .i { | .content span.i { | ||||||
| 	font-style: italic; | 	font-style: italic; | ||||||
| } | } | ||||||
| .content .u { | .content span.u { | ||||||
| 	text-decoration: underline; | 	text-decoration: underline; | ||||||
| } | } | ||||||
| .content .s { | .content span.s { | ||||||
| 	text-decoration: line-through; | 	text-decoration: line-through; | ||||||
| } | } | ||||||
| .content .m { | .content span.m { | ||||||
| 	font-family: monospace; | 	font-family: monospace; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										318
									
								
								xP/public/xP.js
									
									
									
									
									
								
							
							
						
						
									
										318
									
								
								xP/public/xP.js
									
									
									
									
									
								
							| @ -1,175 +1,5 @@ | |||||||
| // Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | // Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | ||||||
| // SPDX-License-Identifier: 0BSD
 | // 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 -----------------------------------------------------------------
 | // --- Colours -----------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
| @ -203,6 +33,69 @@ function applyColor(fg, bg, inverse) { | |||||||
| 		return style | 		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 ---------------------------------------------------------------------
 | // ---- UI ---------------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
| let BufferList = { | let BufferList = { | ||||||
| @ -210,8 +103,8 @@ let BufferList = { | |||||||
| 		let items = [] | 		let items = [] | ||||||
| 		buffers.forEach((b, name) => { | 		buffers.forEach((b, name) => { | ||||||
| 			let attrs = { | 			let attrs = { | ||||||
| 				onclick: event => { | 				onclick: e => { | ||||||
| 					rpc.send({command: 'BufferActivate', bufferName: name}) | 					send({command: 'BufferActivate', bufferName: name}) | ||||||
| 				}, | 				}, | ||||||
| 			} | 			} | ||||||
| 			if (name == bufferCurrent) | 			if (name == bufferCurrent) | ||||||
| @ -222,24 +115,6 @@ 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 = { | let Content = { | ||||||
| 	view: vnode => { | 	view: vnode => { | ||||||
| 		let line = vnode.children[0] | 		let line = vnode.children[0] | ||||||
| @ -264,10 +139,11 @@ let Content = { | |||||||
| 		line.items.forEach(item => { | 		line.items.forEach(item => { | ||||||
| 			switch (item.kind) { | 			switch (item.kind) { | ||||||
| 			case 'Text': | 			case 'Text': | ||||||
| 				linkify(item.text, { | 				// TODO: Detect and transform links.
 | ||||||
|  | 				content.push(m('span', { | ||||||
| 					class: Array.from(classes.keys()).join(' '), | 					class: Array.from(classes.keys()).join(' '), | ||||||
| 					style: applyColor(fg, bg, inverse), | 					style: applyColor(fg, bg, inverse), | ||||||
| 				}, content) | 				}, item.text)) | ||||||
| 				break | 				break | ||||||
| 			case 'Reset': | 			case 'Reset': | ||||||
| 				classes.clear() | 				classes.clear() | ||||||
| @ -303,7 +179,7 @@ let Buffer = { | |||||||
| 
 | 
 | ||||||
| 		let lastDateMark = undefined | 		let lastDateMark = undefined | ||||||
| 		b.lines.forEach(line => { | 		b.lines.forEach(line => { | ||||||
| 			let date = new Date(line.when) | 			let date = new Date(line.when * 1000) | ||||||
| 			let dateMark = date.toLocaleDateString() | 			let dateMark = date.toLocaleDateString() | ||||||
| 			if (dateMark !== lastDateMark) { | 			if (dateMark !== lastDateMark) { | ||||||
| 				lines.push(m('.date', {}, dateMark)) | 				lines.push(m('.date', {}, dateMark)) | ||||||
| @ -320,54 +196,26 @@ 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,
 | // TODO: This should be remembered across buffer switches,
 | ||||||
| // and we'll probably have to intercept /all/ key presses.
 | // and we'll probably have to intercept /all/ key presses.
 | ||||||
| let Input = { | let Input = { | ||||||
| 	view: vnode => { | 	view: vnode => { | ||||||
| 		return m('textarea', { | 		return m('textarea', { | ||||||
| 			rows: 1, | 			rows: 1, | ||||||
| 			onkeydown: onKeyDown, | 			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 = '' | ||||||
|  | 			}, | ||||||
| 		}) | 		}) | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user