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 "" | 	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 "\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 seconds. | 		// Unix timestamp in milliseconds. | ||||||
| 		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,6 +104,8 @@ 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; | ||||||
|  | |||||||
							
								
								
									
										181
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								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'g' & 31, 0 }; | 	wchar_t x[] = { L'c' & 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; | 	e->when = line->when * 1000; | ||||||
| 
 | 
 | ||||||
| 	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,12 +13271,6 @@ 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) | ||||||
| { | { | ||||||
| @ -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 }; | 	self->words[self->words_len++] = (struct completion_word) { start, end }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static struct completion | ||||||
| completion_parse (struct completion *self, const char *line, size_t len) | 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
 | 	// 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; | ||||||
| @ -13309,10 +13303,11 @@ completion_parse (struct completion *self, 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 | ||||||
| @ -13486,14 +13481,13 @@ complete_set (struct app_context *ctx, struct completion *data, | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | 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) | 	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; | ||||||
| 
 | 
 | ||||||
| @ -13509,10 +13503,9 @@ complete_topic (struct app_context *ctx, struct completion *data, | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | 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) | 	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; | ||||||
| @ -13534,9 +13527,9 @@ complete_nicknames (struct app_context *ctx, struct completion *data, | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static char ** | static struct strv | ||||||
| complete_word (struct app_context *ctx, struct completion *data, | complete_word (struct app_context *ctx, struct buffer *buffer, | ||||||
| 	const char *word) | 	struct completion *data, const char *word) | ||||||
| { | { | ||||||
| 	char *initial = completion_word (data, 0); | 	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")) | 	else if (data->location == 1 && !strcmp (initial, "/topic")) | ||||||
| 	{ | 	{ | ||||||
| 		complete_topic     (ctx, data, word, &words); | 		complete_topic     (buffer, data, word, &words); | ||||||
| 		complete_nicknames (ctx, data, word, &words); | 		complete_nicknames (buffer, data, word, &words); | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| 		complete_nicknames (ctx, data, word, &words); | 		complete_nicknames (buffer, 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) | ||||||
| @ -13568,17 +13561,12 @@ complete_word (struct app_context *ctx, struct completion *data, | |||||||
| 		hook->complete (hook, data, word, &words); | 		hook->complete (hook, data, word, &words); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (words.len == 1) | 	if (words.len <= 2) | ||||||
| 	{ |  | ||||||
| 		// 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 | ||||||
| 	{ | 	{ | ||||||
| @ -13589,7 +13577,7 @@ complete_word (struct app_context *ctx, struct completion *data, | |||||||
| 		else | 		else | ||||||
| 			words.vector[0] = xstrndup (words.vector[1], prefix); | 			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); | 	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,
 | /// 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_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 }; | 	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)); | ||||||
| @ -13683,20 +13671,23 @@ make_completions (struct app_context *ctx, char *line, int start, int end) | |||||||
| 
 | 
 | ||||||
| 	hard_assert (start >= 0 && end >= 0 && start <= end); | 	hard_assert (start >= 0 && end >= 0 && start <= end); | ||||||
| 
 | 
 | ||||||
| 	struct completion c; | 	struct strv completions = | ||||||
| 	completion_init (&c); | 		make_completions (ctx, ctx->current_buffer, line_utf8, start, end); | ||||||
| 	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); | ||||||
| 	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 --------------------------------------------
 | // --- 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
 | 	// Don't iterate over filenames and stuff
 | ||||||
| 	rl_attempted_completion_over = true; | 	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 | static int | ||||||
| @ -14425,8 +14416,6 @@ 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...
 | ||||||
| @ -14440,7 +14429,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_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
 | 	// XXX: possibly incorrect wrt. shift state encodings
 | ||||||
| 	copy[el_end] = '\0'; | 	copy[el_end] = '\0'; | ||||||
| @ -14538,12 +14527,6 @@ 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"); | ||||||
| @ -14552,6 +14535,11 @@ 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
 | ||||||
| @ -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 | 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_message *m = relay_prepare (c->ctx); | 	struct relay_event_data_response *e = | ||||||
| 	struct relay_event_data_response *e = &m->data.response; | 		&relay_prepare (c->ctx)->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; | ||||||
| @ -15254,9 +15286,8 @@ 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: | ||||||
| 		// TODO: Run the completion machinery.
 | 		client_process_buffer_complete (c, m->command_seq, buffer, | ||||||
| 		relay_prepare_error (c->ctx, m->command_seq, "Not implemented"); | 			&m->data.buffer_complete); | ||||||
| 		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 span.b { | .content .b { | ||||||
| 	font-weight: bold; | 	font-weight: bold; | ||||||
| } | } | ||||||
| .content span.i { | .content .i { | ||||||
| 	font-style: italic; | 	font-style: italic; | ||||||
| } | } | ||||||
| .content span.u { | .content .u { | ||||||
| 	text-decoration: underline; | 	text-decoration: underline; | ||||||
| } | } | ||||||
| .content span.s { | .content .s { | ||||||
| 	text-decoration: line-through; | 	text-decoration: line-through; | ||||||
| } | } | ||||||
| .content span.m { | .content .m { | ||||||
| 	font-family: monospace; | 	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>
 | // 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 -----------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
| @ -33,69 +203,6 @@ 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 = { | ||||||
| @ -103,8 +210,8 @@ let BufferList = { | |||||||
| 		let items = [] | 		let items = [] | ||||||
| 		buffers.forEach((b, name) => { | 		buffers.forEach((b, name) => { | ||||||
| 			let attrs = { | 			let attrs = { | ||||||
| 				onclick: e => { | 				onclick: event => { | ||||||
| 					send({command: 'BufferActivate', bufferName: name}) | 					rpc.send({command: 'BufferActivate', bufferName: name}) | ||||||
| 				}, | 				}, | ||||||
| 			} | 			} | ||||||
| 			if (name == bufferCurrent) | 			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 = { | let Content = { | ||||||
| 	view: vnode => { | 	view: vnode => { | ||||||
| 		let line = vnode.children[0] | 		let line = vnode.children[0] | ||||||
| @ -139,11 +264,10 @@ let Content = { | |||||||
| 		line.items.forEach(item => { | 		line.items.forEach(item => { | ||||||
| 			switch (item.kind) { | 			switch (item.kind) { | ||||||
| 			case 'Text': | 			case 'Text': | ||||||
| 				// TODO: Detect and transform links.
 | 				linkify(item.text, { | ||||||
| 				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), | ||||||
| 				}, item.text)) | 				}, content) | ||||||
| 				break | 				break | ||||||
| 			case 'Reset': | 			case 'Reset': | ||||||
| 				classes.clear() | 				classes.clear() | ||||||
| @ -179,7 +303,7 @@ let Buffer = { | |||||||
| 
 | 
 | ||||||
| 		let lastDateMark = undefined | 		let lastDateMark = undefined | ||||||
| 		b.lines.forEach(line => { | 		b.lines.forEach(line => { | ||||||
| 			let date = new Date(line.when * 1000) | 			let date = new Date(line.when) | ||||||
| 			let dateMark = date.toLocaleDateString() | 			let dateMark = date.toLocaleDateString() | ||||||
| 			if (dateMark !== lastDateMark) { | 			if (dateMark !== lastDateMark) { | ||||||
| 				lines.push(m('.date', {}, dateMark)) | 				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,
 | // 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: e => { | 			onkeydown: onKeyDown, | ||||||
| 				// 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