Compare commits
	
		
			5 Commits
		
	
	
		
			0bc2c12eec
			...
			62773acaa0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 62773acaa0 | |||
| 7e3919e25d | |||
| 4bc2f736f2 | |||
| add670212f | |||
| 95aa89ee97 | 
							
								
								
									
										6
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								NEWS
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| Unreleased | 2.0.0 (Unreleased) | ||||||
| 
 | 
 | ||||||
|  * xD: implemented WALLOPS, choosing to make it target even non-operators |  * xD: implemented WALLOPS, choosing to make it target even non-operators | ||||||
| 
 | 
 | ||||||
| @ -8,6 +8,10 @@ Unreleased | |||||||
|    with the exception of editor_command/editor, backlog_helper/pager, |    with the exception of editor_command/editor, backlog_helper/pager, | ||||||
|    and backlog_helper_strip_formatting/pager_strip_formatting |    and backlog_helper_strip_formatting/pager_strip_formatting | ||||||
| 
 | 
 | ||||||
|  |  * xC: all attributes.* configuration options have been made abstract in | ||||||
|  |    a subset of the git-config(1) format, and renamed to theme.*, | ||||||
|  |    with the exception of attributes.reset, which has no replacement | ||||||
|  | 
 | ||||||
|  * xC: replaced behaviour.save_on_quit with general.autosave |  * xC: replaced behaviour.save_on_quit with general.autosave | ||||||
| 
 | 
 | ||||||
|  * xC: improved pager integration capabilities |  * xC: improved pager integration capabilities | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.adoc
									
									
									
									
									
								
							| @ -26,12 +26,13 @@ As a unique bonus, you can launch a full text editor from within. | |||||||
| xP | xP | ||||||
| -- | -- | ||||||
| The web frontend for 'xC', making use of its networked relay interface. | The web frontend for 'xC', making use of its networked relay interface. | ||||||
| So far it's somewhat basic, yet usable. | So far it's a bit rough around the edges, yet fully usable. | ||||||
| 
 | 
 | ||||||
| xF | xF | ||||||
| -- | -- | ||||||
| The X11 frontend for 'xC', making use of its networked relay interface. | The X11 frontend for 'xC', making use of its networked relay interface. | ||||||
| It's currently in development, and hidden behind a CMake option. | This subproject has been put on hold, partly because of its massive overlap | ||||||
|  | with 'xP', and is hidden behind a CMake option. | ||||||
| 
 | 
 | ||||||
| xD | xD | ||||||
| -- | -- | ||||||
| @ -117,8 +118,19 @@ as a `forking` type systemd user service. | |||||||
| 
 | 
 | ||||||
| xP | xP | ||||||
| ~~ | ~~ | ||||||
| Install the Go compiler, and build the server using `make` in its directory, | The precondition for running 'xC' frontends is enabling its relay interface: | ||||||
| then run it from within the _public_ subdirectory. | 
 | ||||||
|  |  /set general.relay_bind = "127.0.0.1:9000" | ||||||
|  | 
 | ||||||
|  | To build the web server, you'll need to install the Go compiler, and run `make` | ||||||
|  | from the _xP_ directory.  Then start it from the _public_ subdirectory, | ||||||
|  | and navigate to the adress you gave it as its first argument--in the following | ||||||
|  | example, that would be http://localhost:8080[]: | ||||||
|  | 
 | ||||||
|  |  $ ../xP 127.0.0.1:8080 127.0.0.1:9000 | ||||||
|  | 
 | ||||||
|  | For remote use, it's recommended to put 'xP' behind a reverse proxy, with TLS, | ||||||
|  | and some form of HTTP authentication. | ||||||
| 
 | 
 | ||||||
| Client Certificates | Client Certificates | ||||||
| ------------------- | ------------------- | ||||||
| @ -165,12 +177,12 @@ properly set-up terminal emulator, it suffices to run: | |||||||
|  /set general.pager = Press Tab here and change +Gb to +Gb1d |  /set general.pager = Press Tab here and change +Gb to +Gb1d | ||||||
|  /set general.date_change_line = "%a %e %b %Y" |  /set general.date_change_line = "%a %e %b %Y" | ||||||
|  /set general.plugin_autoload += "fancy-prompt.lua" |  /set general.plugin_autoload += "fancy-prompt.lua" | ||||||
|  /set attributes.userhost = "\x1b[38;5;109m" |  /set theme.userhost = "109" | ||||||
|  /set attributes.join = "\x1b[38;5;108m" |  /set theme.join = "108" | ||||||
|  /set attributes.part = "\x1b[38;5;138m" |  /set theme.part = "138" | ||||||
|  /set attributes.external = "\x1b[38;5;248m" |  /set theme.external = "248" | ||||||
|  /set attributes.timestamp = "\x1b[48;5;255m\x1b[38;5;250m" |  /set theme.timestamp = "250 255" | ||||||
|  /set attributes.read_marker = "\x1b[38;5;202m" |  /set theme.read_marker = "202" | ||||||
| 
 | 
 | ||||||
| Configuration profiles | Configuration profiles | ||||||
| ---------------------- | ---------------------- | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								liberty
									
									
									
									
									
								
							| @ -1 +1 @@ | |||||||
| Subproject commit f545be725df9195a5b5897ad95a0220acf10f148 | Subproject commit 22a121383f73fa7739f324021b6ad0ba6ed3cdb3 | ||||||
							
								
								
									
										491
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										491
									
								
								xC.c
									
									
									
									
									
								
							| @ -19,7 +19,6 @@ | |||||||
| // A table of all attributes we use for output
 | // A table of all attributes we use for output
 | ||||||
| #define ATTR_TABLE(XX)                                                  \ | #define ATTR_TABLE(XX)                                                  \ | ||||||
| 	XX( PROMPT,      prompt,      Terminal attrs for the prompt       ) \ | 	XX( PROMPT,      prompt,      Terminal attrs for the prompt       ) \ | ||||||
| 	XX( RESET,       reset,       String to reset terminal attributes ) \ |  | ||||||
| 	XX( DATE_CHANGE, date_change, Terminal attrs for date change      ) \ | 	XX( DATE_CHANGE, date_change, Terminal attrs for date change      ) \ | ||||||
| 	XX( READ_MARKER, read_marker, Terminal attrs for the read marker  ) \ | 	XX( READ_MARKER, read_marker, Terminal attrs for the read marker  ) \ | ||||||
| 	XX( WARNING,     warning,     Terminal attrs for warnings         ) \ | 	XX( WARNING,     warning,     Terminal attrs for warnings         ) \ | ||||||
| @ -34,6 +33,7 @@ | |||||||
| 
 | 
 | ||||||
| enum | enum | ||||||
| { | { | ||||||
|  | 	ATTR_RESET, | ||||||
| #define XX(x, y, z) ATTR_ ## x, | #define XX(x, y, z) ATTR_ ## x, | ||||||
| 	ATTR_TABLE (XX) | 	ATTR_TABLE (XX) | ||||||
| #undef XX | #undef XX | ||||||
| @ -48,6 +48,9 @@ enum | |||||||
| #include "config.h" | #include "config.h" | ||||||
| #define PROGRAM_NAME "xC" | #define PROGRAM_NAME "xC" | ||||||
| 
 | 
 | ||||||
|  | // fmemopen
 | ||||||
|  | #define _POSIX_C_SOURCE 200809L | ||||||
|  | 
 | ||||||
| #include "common.c" | #include "common.c" | ||||||
| #include "xD-replies.c" | #include "xD-replies.c" | ||||||
| #include "xC-proto.c" | #include "xC-proto.c" | ||||||
| @ -1452,6 +1455,58 @@ channel_destroy (struct channel *self) | |||||||
| 
 | 
 | ||||||
| REF_COUNTABLE_METHODS (channel) | REF_COUNTABLE_METHODS (channel) | ||||||
| 
 | 
 | ||||||
|  | // ~~~ Attribute utilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | ||||||
|  | 
 | ||||||
|  | enum | ||||||
|  | { | ||||||
|  | 	TEXT_BOLD        = 1 << 0, | ||||||
|  | 	TEXT_ITALIC      = 1 << 1, | ||||||
|  | 	TEXT_UNDERLINE   = 1 << 2, | ||||||
|  | 	TEXT_INVERSE     = 1 << 3, | ||||||
|  | 	TEXT_BLINK       = 1 << 4, | ||||||
|  | 	TEXT_CROSSED_OUT = 1 << 5, | ||||||
|  | 	TEXT_MONOSPACE   = 1 << 6 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Similar to code in liberty-tui.c.
 | ||||||
|  | struct attrs | ||||||
|  | { | ||||||
|  | 	short fg;                           ///< Foreground (256-colour cube or -1)
 | ||||||
|  | 	short bg;                           ///< Background (256-colour cube or -1)
 | ||||||
|  | 	unsigned attrs;                     ///< TEXT_* mask
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /// Decode attributes in the value using a subset of the git config format,
 | ||||||
|  | /// ignoring all errors since it doesn't affect functionality
 | ||||||
|  | static struct attrs | ||||||
|  | attrs_decode (const char *value) | ||||||
|  | { | ||||||
|  | 	struct strv v = strv_make (); | ||||||
|  | 	cstr_split (value, " ", true, &v); | ||||||
|  | 
 | ||||||
|  | 	int colors = 0; | ||||||
|  | 	struct attrs attrs = { -1, -1, 0 }; | ||||||
|  | 	for (char **it = v.vector; *it; it++) | ||||||
|  | 	{ | ||||||
|  | 		char *end = NULL; | ||||||
|  | 		long n = strtol (*it, &end, 10); | ||||||
|  | 		if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX) | ||||||
|  | 		{ | ||||||
|  | 			if (colors == 0) attrs.fg = n; | ||||||
|  | 			if (colors == 1) attrs.bg = n; | ||||||
|  | 			colors++; | ||||||
|  | 		} | ||||||
|  | 		else if (!strcmp (*it, "bold"))    attrs.attrs |= TEXT_BOLD; | ||||||
|  | 		else if (!strcmp (*it, "italic"))  attrs.attrs |= TEXT_ITALIC; | ||||||
|  | 		else if (!strcmp (*it, "ul"))      attrs.attrs |= TEXT_UNDERLINE; | ||||||
|  | 		else if (!strcmp (*it, "reverse")) attrs.attrs |= TEXT_INVERSE; | ||||||
|  | 		else if (!strcmp (*it, "blink"))   attrs.attrs |= TEXT_BLINK; | ||||||
|  | 		else if (!strcmp (*it, "strike"))  attrs.attrs |= TEXT_CROSSED_OUT; | ||||||
|  | 	} | ||||||
|  | 	strv_free (&v); | ||||||
|  | 	return attrs; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | // ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | ||||||
| 
 | 
 | ||||||
| enum formatter_item_type | enum formatter_item_type | ||||||
| @ -1465,17 +1520,6 @@ enum formatter_item_type | |||||||
| 	FORMATTER_ITEM_IGNORE_ATTR          ///< Un/set attribute ignoration
 | 	FORMATTER_ITEM_IGNORE_ATTR          ///< Un/set attribute ignoration
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| enum |  | ||||||
| { |  | ||||||
| 	TEXT_BOLD        = 1 << 0, |  | ||||||
| 	TEXT_ITALIC      = 1 << 1, |  | ||||||
| 	TEXT_UNDERLINE   = 1 << 2, |  | ||||||
| 	TEXT_INVERSE     = 1 << 3, |  | ||||||
| 	TEXT_BLINK       = 1 << 4, |  | ||||||
| 	TEXT_CROSSED_OUT = 1 << 5, |  | ||||||
| 	TEXT_MONOSPACE   = 1 << 6 |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| struct formatter_item | struct formatter_item | ||||||
| { | { | ||||||
| 	enum formatter_item_type type : 16; ///< Type of this item
 | 	enum formatter_item_type type : 16; ///< Type of this item
 | ||||||
| @ -2116,12 +2160,13 @@ struct completion_hook | |||||||
| 
 | 
 | ||||||
| struct app_context | struct app_context | ||||||
| { | { | ||||||
| 	char *attrs_defaults[ATTR_COUNT];   ///< Default terminal attributes
 | 	/// Default terminal attributes
 | ||||||
|  | 	struct attrs theme_defaults[ATTR_COUNT]; | ||||||
| 
 | 
 | ||||||
| 	// Configuration:
 | 	// Configuration:
 | ||||||
| 
 | 
 | ||||||
| 	struct config config;               ///< Program configuration
 | 	struct config config;               ///< Program configuration
 | ||||||
| 	char *attrs[ATTR_COUNT];            ///< Terminal attributes
 | 	struct attrs theme[ATTR_COUNT];     ///< Terminal attributes
 | ||||||
| 	bool isolate_buffers;               ///< Isolate global/server buffers
 | 	bool isolate_buffers;               ///< Isolate global/server buffers
 | ||||||
| 	bool beep_on_highlight;             ///< Beep on highlight
 | 	bool beep_on_highlight;             ///< Beep on highlight
 | ||||||
| 	bool logging;                       ///< Logging to file enabled
 | 	bool logging;                       ///< Logging to file enabled
 | ||||||
| @ -2305,11 +2350,6 @@ app_context_free (struct app_context *self) | |||||||
| 		plugin_destroy (iter); | 		plugin_destroy (iter); | ||||||
| 
 | 
 | ||||||
| 	config_free (&self->config); | 	config_free (&self->config); | ||||||
| 	for (size_t i = 0; i < ATTR_COUNT; i++) |  | ||||||
| 	{ |  | ||||||
| 		free (self->attrs_defaults[i]); |  | ||||||
| 		free (self->attrs[i]); |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	LIST_FOR_EACH (struct buffer, iter, self->buffers) | 	LIST_FOR_EACH (struct buffer, iter, self->buffers) | ||||||
| 	{ | 	{ | ||||||
| @ -2367,7 +2407,7 @@ on_config_show_all_prefixes_change (struct config_item *item) | |||||||
| 
 | 
 | ||||||
| static void on_config_relay_bind_change (struct config_item *item); | static void on_config_relay_bind_change (struct config_item *item); | ||||||
| static void on_config_backlog_limit_change (struct config_item *item); | static void on_config_backlog_limit_change (struct config_item *item); | ||||||
| static void on_config_attribute_change (struct config_item *item); | static void on_config_theme_change (struct config_item *item); | ||||||
| static void on_config_logging_change (struct config_item *item); | static void on_config_logging_change (struct config_item *item); | ||||||
| 
 | 
 | ||||||
| #define TRIVIAL_BOOLEAN_ON_CHANGE(name)                                        \ | #define TRIVIAL_BOOLEAN_ON_CHANGE(name)                                        \ | ||||||
| @ -2648,10 +2688,10 @@ static struct config_schema g_config_general[] = | |||||||
| 	{} | 	{} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static struct config_schema g_config_attributes[] = | static struct config_schema g_config_theme[] = | ||||||
| { | { | ||||||
| #define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \ | #define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \ | ||||||
| 	.on_change = on_config_attribute_change }, | 	.on_change = on_config_theme_change }, | ||||||
| 	ATTR_TABLE (XX) | 	ATTR_TABLE (XX) | ||||||
| #undef XX | #undef XX | ||||||
| 	{} | 	{} | ||||||
| @ -2666,9 +2706,9 @@ load_config_general (struct config_item *subtree, void *user_data) | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| load_config_attributes (struct config_item *subtree, void *user_data) | load_config_theme (struct config_item *subtree, void *user_data) | ||||||
| { | { | ||||||
| 	config_schema_apply_to_object (g_config_attributes, subtree, user_data); | 	config_schema_apply_to_object (g_config_theme, subtree, user_data); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| @ -2680,7 +2720,7 @@ register_config_modules (struct app_context *ctx) | |||||||
| 	config_register_module (config, "aliases", NULL, NULL); | 	config_register_module (config, "aliases", NULL, NULL); | ||||||
| 	config_register_module (config, "plugins", NULL, NULL); | 	config_register_module (config, "plugins", NULL, NULL); | ||||||
| 	config_register_module (config, "general", load_config_general, ctx); | 	config_register_module (config, "general", load_config_general, ctx); | ||||||
| 	config_register_module (config, "attributes", load_config_attributes, ctx); | 	config_register_module (config, "theme",   load_config_theme,   ctx); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
| @ -3114,6 +3154,64 @@ relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer) | |||||||
| 	e->buffer_name = str_from_cstr (buffer->name); | 	e->buffer_name = str_from_cstr (buffer->name); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static union relay_item_data * | ||||||
|  | relay_translate_formatter (struct app_context *ctx, union relay_item_data *p, | ||||||
|  | 	struct formatter_item *i) | ||||||
|  | { | ||||||
|  | 	// XXX: See attr_printer_decode_color(), this is a footgun.
 | ||||||
|  | 	int16_t c16  = i->color; | ||||||
|  | 	int16_t c256 = i->color >> 16; | ||||||
|  | 
 | ||||||
|  | 	unsigned attrs = i->attribute; | ||||||
|  | 	switch (i->type) | ||||||
|  | 	{ | ||||||
|  | 	case FORMATTER_ITEM_TEXT: | ||||||
|  | 		p->text.text = str_from_cstr (i->text); | ||||||
|  | 		(p++)->kind = RELAY_ITEM_TEXT; | ||||||
|  | 		break; | ||||||
|  | 	case FORMATTER_ITEM_FG_COLOR: | ||||||
|  | 		p->fg_color.color = c256 <= 0 ? c16 : c256; | ||||||
|  | 		(p++)->kind = RELAY_ITEM_FG_COLOR; | ||||||
|  | 		break; | ||||||
|  | 	case FORMATTER_ITEM_BG_COLOR: | ||||||
|  | 		p->bg_color.color = c256 <= 0 ? c16 : c256; | ||||||
|  | 		(p++)->kind = RELAY_ITEM_BG_COLOR; | ||||||
|  | 		break; | ||||||
|  | 	case FORMATTER_ITEM_ATTR: | ||||||
|  | 		(p++)->kind = RELAY_ITEM_RESET; | ||||||
|  | 		if ((c256 = ctx->theme[i->attribute].fg) >= 0) | ||||||
|  | 		{ | ||||||
|  | 			p->fg_color.color = c256; | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FG_COLOR; | ||||||
|  | 		} | ||||||
|  | 		if ((c256 = ctx->theme[i->attribute].bg) >= 0) | ||||||
|  | 		{ | ||||||
|  | 			p->bg_color.color = c256; | ||||||
|  | 			(p++)->kind = RELAY_ITEM_BG_COLOR; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		attrs = ctx->theme[i->attribute].attrs; | ||||||
|  | 		// Fall-through
 | ||||||
|  | 	case FORMATTER_ITEM_SIMPLE: | ||||||
|  | 		if (attrs & TEXT_BOLD) | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FLIP_BOLD; | ||||||
|  | 		if (attrs & TEXT_ITALIC) | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FLIP_ITALIC; | ||||||
|  | 		if (attrs & TEXT_UNDERLINE) | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FLIP_UNDERLINE; | ||||||
|  | 		if (attrs & TEXT_INVERSE) | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FLIP_INVERSE; | ||||||
|  | 		if (attrs & TEXT_CROSSED_OUT) | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT; | ||||||
|  | 		if (attrs & TEXT_MONOSPACE) | ||||||
|  | 			(p++)->kind = RELAY_ITEM_FLIP_MONOSPACE; | ||||||
|  | 		break; | ||||||
|  | 	default: | ||||||
|  | 		break; | ||||||
|  | 	} | ||||||
|  | 	return p; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void | static void | ||||||
| relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer, | relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer, | ||||||
| 	struct buffer_line *line, bool leak_to_active) | 	struct buffer_line *line, bool leak_to_active) | ||||||
| @ -3132,49 +3230,10 @@ relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer, | |||||||
| 	for (size_t i = 0; line->items[i].type; i++) | 	for (size_t i = 0; line->items[i].type; i++) | ||||||
| 		len++; | 		len++; | ||||||
| 
 | 
 | ||||||
| 	// XXX: This way helps xP's JSON conversion, but is super annoying for us.
 | 	// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
 | ||||||
| 	union relay_item_data *p = e->items = xcalloc (len * 6, sizeof *e->items); | 	union relay_item_data *p = e->items = xcalloc (len * 9, sizeof *e->items); | ||||||
| 	for (struct formatter_item *i = line->items; len--; i++) | 	for (struct formatter_item *i = line->items; len--; i++) | ||||||
| 	{ | 		p = relay_translate_formatter (ctx, p, i); | ||||||
| 		// XXX: See attr_printer_decode_color(), this is a footgun.
 |  | ||||||
| 		int16_t c16  = i->color; |  | ||||||
| 		int16_t c256 = i->color >> 16; |  | ||||||
| 		switch (i->type) |  | ||||||
| 		{ |  | ||||||
| 		case FORMATTER_ITEM_TEXT: |  | ||||||
| 			p->text.text = str_from_cstr (i->text); |  | ||||||
| 			(p++)->kind = RELAY_ITEM_TEXT; |  | ||||||
| 			break; |  | ||||||
| 		case FORMATTER_ITEM_ATTR: |  | ||||||
| 			// For future consideration.
 |  | ||||||
| 			(p++)->kind = RELAY_ITEM_RESET; |  | ||||||
| 			break; |  | ||||||
| 		case FORMATTER_ITEM_FG_COLOR: |  | ||||||
| 			p->fg_color.color = c256 <= 0 ? c16 : c256; |  | ||||||
| 			(p++)->kind = RELAY_ITEM_FG_COLOR; |  | ||||||
| 			break; |  | ||||||
| 		case FORMATTER_ITEM_BG_COLOR: |  | ||||||
| 			p->bg_color.color = c256 <= 0 ? c16 : c256; |  | ||||||
| 			(p++)->kind = RELAY_ITEM_BG_COLOR; |  | ||||||
| 			break; |  | ||||||
| 		case FORMATTER_ITEM_SIMPLE: |  | ||||||
| 			if (i->attribute & TEXT_BOLD) |  | ||||||
| 				(p++)->kind = RELAY_ITEM_FLIP_BOLD; |  | ||||||
| 			if (i->attribute & TEXT_ITALIC) |  | ||||||
| 				(p++)->kind = RELAY_ITEM_FLIP_ITALIC; |  | ||||||
| 			if (i->attribute & TEXT_UNDERLINE) |  | ||||||
| 				(p++)->kind = RELAY_ITEM_FLIP_UNDERLINE; |  | ||||||
| 			if (i->attribute & TEXT_INVERSE) |  | ||||||
| 				(p++)->kind = RELAY_ITEM_FLIP_INVERSE; |  | ||||||
| 			if (i->attribute & TEXT_CROSSED_OUT) |  | ||||||
| 				(p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT; |  | ||||||
| 			if (i->attribute & TEXT_MONOSPACE) |  | ||||||
| 				(p++)->kind = RELAY_ITEM_FLIP_MONOSPACE; |  | ||||||
| 			break; |  | ||||||
| 		default: |  | ||||||
| 			break; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	e->items_len = p - e->items; | 	e->items_len = p - e->items; | ||||||
| } | } | ||||||
| @ -3239,123 +3298,6 @@ get_attribute_printer (FILE *stream) | |||||||
| 	return NULL; | 	return NULL; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void |  | ||||||
| vprint_attributed (struct app_context *ctx, |  | ||||||
| 	FILE *stream, intptr_t attribute, const char *fmt, va_list ap) |  | ||||||
| { |  | ||||||
| 	terminal_printer_fn printer = get_attribute_printer (stream); |  | ||||||
| 	if (!attribute) |  | ||||||
| 		printer = NULL; |  | ||||||
| 
 |  | ||||||
| 	if (printer) |  | ||||||
| 		tputs (ctx->attrs[attribute], 1, printer); |  | ||||||
| 
 |  | ||||||
| 	vfprintf (stream, fmt, ap); |  | ||||||
| 
 |  | ||||||
| 	if (printer) |  | ||||||
| 		tputs (ctx->attrs[ATTR_RESET], 1, printer); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void |  | ||||||
| print_attributed (struct app_context *ctx, |  | ||||||
| 	FILE *stream, intptr_t attribute, const char *fmt, ...) |  | ||||||
| { |  | ||||||
| 	va_list ap; |  | ||||||
| 	va_start (ap, fmt); |  | ||||||
| 	vprint_attributed (ctx, stream, attribute, fmt, ap); |  | ||||||
| 	va_end (ap); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void |  | ||||||
| log_message_attributed (void *user_data, const char *quote, const char *fmt, |  | ||||||
| 	va_list ap) |  | ||||||
| { |  | ||||||
| 	FILE *stream = stderr; |  | ||||||
| 	struct app_context *ctx = g_ctx; |  | ||||||
| 
 |  | ||||||
| 	CALL (ctx->input, hide); |  | ||||||
| 
 |  | ||||||
| 	print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote); |  | ||||||
| 	vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap); |  | ||||||
| 	fputs ("\n", stream); |  | ||||||
| 
 |  | ||||||
| 	CALL (ctx->input, show); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static ssize_t |  | ||||||
| attr_by_name (const char *name) |  | ||||||
| { |  | ||||||
| 	static const char *table[ATTR_COUNT] = |  | ||||||
| 	{ |  | ||||||
| #define XX(x, y, z) [ATTR_ ## x] = #y, |  | ||||||
| 		ATTR_TABLE (XX) |  | ||||||
| #undef XX |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	for (size_t i = 0; i < N_ELEMENTS (table); i++) |  | ||||||
| 		if (!strcmp (name, table[i])) |  | ||||||
| 			return i; |  | ||||||
| 	return -1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void |  | ||||||
| on_config_attribute_change (struct config_item *item) |  | ||||||
| { |  | ||||||
| 	struct app_context *ctx = item->user_data; |  | ||||||
| 	ssize_t id = attr_by_name (item->schema->name); |  | ||||||
| 	if (id != -1) |  | ||||||
| 	{ |  | ||||||
| 		cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL |  | ||||||
| 			? ctx->attrs_defaults[id] |  | ||||||
| 			: item->value.string.str)); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void |  | ||||||
| init_colors (struct app_context *ctx) |  | ||||||
| { |  | ||||||
| 	bool have_ti = init_terminal (); |  | ||||||
| 	char **defaults = ctx->attrs_defaults; |  | ||||||
| 
 |  | ||||||
| #define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "") |  | ||||||
| 
 |  | ||||||
| 	INIT_ATTR (PROMPT,      enter_bold_mode); |  | ||||||
| 	INIT_ATTR (RESET,       exit_attribute_mode); |  | ||||||
| 	INIT_ATTR (DATE_CHANGE, enter_bold_mode); |  | ||||||
| 	INIT_ATTR (READ_MARKER, g_terminal.color_set_fg[COLOR_MAGENTA]); |  | ||||||
| 	INIT_ATTR (WARNING,     g_terminal.color_set_fg[COLOR_YELLOW]); |  | ||||||
| 	INIT_ATTR (ERROR,       g_terminal.color_set_fg[COLOR_RED]); |  | ||||||
| 
 |  | ||||||
| 	INIT_ATTR (EXTERNAL,    g_terminal.color_set_fg[COLOR_WHITE]); |  | ||||||
| 	INIT_ATTR (TIMESTAMP,   g_terminal.color_set_fg[COLOR_WHITE]); |  | ||||||
| 	INIT_ATTR (ACTION,      g_terminal.color_set_fg[COLOR_RED]); |  | ||||||
| 	INIT_ATTR (USERHOST,    g_terminal.color_set_fg[COLOR_CYAN]); |  | ||||||
| 	INIT_ATTR (JOIN,        g_terminal.color_set_fg[COLOR_GREEN]); |  | ||||||
| 	INIT_ATTR (PART,        g_terminal.color_set_fg[COLOR_RED]); |  | ||||||
| 
 |  | ||||||
| 	char *highlight = have_ti ? xstrdup_printf ("%s%s%s", |  | ||||||
| 		g_terminal.color_set_fg[COLOR_YELLOW], |  | ||||||
| 		g_terminal.color_set_bg[COLOR_MAGENTA], |  | ||||||
| 		enter_bold_mode) : NULL; |  | ||||||
| 	INIT_ATTR (HIGHLIGHT, highlight); |  | ||||||
| 	free (highlight); |  | ||||||
| 
 |  | ||||||
| #undef INIT_ATTR |  | ||||||
| 
 |  | ||||||
| 	// This prevents formatters from obtaining an attribute printer function
 |  | ||||||
| 	if (!have_ti) |  | ||||||
| 	{ |  | ||||||
| 		g_terminal.stdout_is_tty = false; |  | ||||||
| 		g_terminal.stderr_is_tty = false; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	g_log_message_real = log_message_attributed; |  | ||||||
| 
 |  | ||||||
| 	// Apply the default values so that we start with any formatting at all
 |  | ||||||
| 	config_schema_call_changed |  | ||||||
| 		(config_item_get (ctx->config.root, "attributes", NULL)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | // ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | ||||||
| 
 | 
 | ||||||
| // A little tool that tries to make the most of the terminal's capabilities
 | // A little tool that tries to make the most of the terminal's capabilities
 | ||||||
| @ -3365,12 +3307,12 @@ init_colors (struct app_context *ctx) | |||||||
| 
 | 
 | ||||||
| struct attr_printer | struct attr_printer | ||||||
| { | { | ||||||
| 	char **attrs;                       ///< Named attributes
 | 	struct attrs *attrs;                ///< Named attributes
 | ||||||
| 	FILE *stream;                       ///< Output stream
 | 	FILE *stream;                       ///< Output stream
 | ||||||
| 	bool dirty;                         ///< Attributes are set
 | 	bool dirty;                         ///< Attributes are set
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #define ATTR_PRINTER_INIT(ctx, stream) { ctx->attrs, stream, true } | #define ATTR_PRINTER_INIT(attrs, stream) { attrs, stream, true } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| attr_printer_filtered_puts (FILE *stream, const char *attr) | attr_printer_filtered_puts (FILE *stream, const char *attr) | ||||||
| @ -3408,22 +3350,11 @@ static void | |||||||
| attr_printer_reset (struct attr_printer *self) | attr_printer_reset (struct attr_printer *self) | ||||||
| { | { | ||||||
| 	if (self->dirty) | 	if (self->dirty) | ||||||
| 		attr_printer_tputs (self, self->attrs[ATTR_RESET]); | 		attr_printer_tputs (self, exit_attribute_mode); | ||||||
| 
 | 
 | ||||||
| 	self->dirty = false; | 	self->dirty = false; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void |  | ||||||
| attr_printer_apply_named (struct attr_printer *self, int attribute) |  | ||||||
| { |  | ||||||
| 	attr_printer_reset (self); |  | ||||||
| 	if (attribute != ATTR_RESET) |  | ||||||
| 	{ |  | ||||||
| 		attr_printer_tputs (self, self->attrs[attribute]); |  | ||||||
| 		self->dirty = true; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NOTE: commonly terminals have:
 | // NOTE: commonly terminals have:
 | ||||||
| //   8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
 | //   8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
 | ||||||
| //   16 colours (okayish, we have the full basic range guaranteed)
 | //   16 colours (okayish, we have the full basic range guaranteed)
 | ||||||
| @ -3551,6 +3482,135 @@ attr_printer_apply (struct attr_printer *self, | |||||||
| 	self->dirty = true; | 	self->dirty = true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static void | ||||||
|  | attr_printer_apply_named (struct attr_printer *self, int attribute) | ||||||
|  | { | ||||||
|  | 	attr_printer_reset (self); | ||||||
|  | 	if (attribute == ATTR_RESET) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	// See the COLOR_256 macro or attr_printer_decode_color().
 | ||||||
|  | 	struct attrs *a = &self->attrs[attribute]; | ||||||
|  | 	attr_printer_apply (self, a->attrs, | ||||||
|  | 		a->fg < 16 ? a->fg : (a->fg << 16 | (-1 & 0xFFFF)), | ||||||
|  | 		a->bg < 16 ? a->bg : (a->bg << 16 | (-1 & 0xFFFF))); | ||||||
|  | 	self->dirty = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ~~~ Logging redirect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | vprint_attributed (struct app_context *ctx, | ||||||
|  | 	FILE *stream, intptr_t attribute, const char *fmt, va_list ap) | ||||||
|  | { | ||||||
|  | 	terminal_printer_fn printer = get_attribute_printer (stream); | ||||||
|  | 	if (!attribute) | ||||||
|  | 		printer = NULL; | ||||||
|  | 
 | ||||||
|  | 	struct attr_printer state = ATTR_PRINTER_INIT (ctx->theme, stream); | ||||||
|  | 	if (printer) | ||||||
|  | 		attr_printer_apply_named (&state, attribute); | ||||||
|  | 
 | ||||||
|  | 	vfprintf (stream, fmt, ap); | ||||||
|  | 
 | ||||||
|  | 	if (printer) | ||||||
|  | 		attr_printer_reset (&state); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | print_attributed (struct app_context *ctx, | ||||||
|  | 	FILE *stream, intptr_t attribute, const char *fmt, ...) | ||||||
|  | { | ||||||
|  | 	va_list ap; | ||||||
|  | 	va_start (ap, fmt); | ||||||
|  | 	vprint_attributed (ctx, stream, attribute, fmt, ap); | ||||||
|  | 	va_end (ap); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | log_message_attributed (void *user_data, const char *quote, const char *fmt, | ||||||
|  | 	va_list ap) | ||||||
|  | { | ||||||
|  | 	FILE *stream = stderr; | ||||||
|  | 	struct app_context *ctx = g_ctx; | ||||||
|  | 
 | ||||||
|  | 	CALL (ctx->input, hide); | ||||||
|  | 
 | ||||||
|  | 	print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote); | ||||||
|  | 	vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap); | ||||||
|  | 	fputs ("\n", stream); | ||||||
|  | 
 | ||||||
|  | 	CALL (ctx->input, show); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ~~~ Theme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | ||||||
|  | 
 | ||||||
|  | static ssize_t | ||||||
|  | attr_by_name (const char *name) | ||||||
|  | { | ||||||
|  | 	static const char *table[ATTR_COUNT] = | ||||||
|  | 	{ | ||||||
|  | 		NULL, | ||||||
|  | #define XX(x, y, z) [ATTR_ ## x] = #y, | ||||||
|  | 		ATTR_TABLE (XX) | ||||||
|  | #undef XX | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	for (size_t i = 1; i < N_ELEMENTS (table); i++) | ||||||
|  | 		if (!strcmp (name, table[i])) | ||||||
|  | 			return i; | ||||||
|  | 	return -1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | on_config_theme_change (struct config_item *item) | ||||||
|  | { | ||||||
|  | 	struct app_context *ctx = item->user_data; | ||||||
|  | 	ssize_t id = attr_by_name (item->schema->name); | ||||||
|  | 	if (id != -1) | ||||||
|  | 	{ | ||||||
|  | 		// TODO: There should be a validator.
 | ||||||
|  | 		ctx->theme[id] = item->type == CONFIG_ITEM_NULL | ||||||
|  | 			? ctx->theme_defaults[id] | ||||||
|  | 			: attrs_decode (item->value.string.str); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void | ||||||
|  | init_colors (struct app_context *ctx) | ||||||
|  | { | ||||||
|  | 	bool have_ti = init_terminal (); | ||||||
|  | 
 | ||||||
|  | #define INIT_ATTR(id, ...) ctx->theme[ATTR_ ## id] = \ | ||||||
|  | 	ctx->theme_defaults[ATTR_ ## id] = (struct attrs) { __VA_ARGS__ } | ||||||
|  | 
 | ||||||
|  | 	INIT_ATTR (PROMPT,      -1, -1, TEXT_BOLD); | ||||||
|  | 	INIT_ATTR (RESET,       -1, -1); | ||||||
|  | 	INIT_ATTR (DATE_CHANGE, -1, -1, TEXT_BOLD); | ||||||
|  | 	INIT_ATTR (READ_MARKER, COLOR_MAGENTA, -1); | ||||||
|  | 	INIT_ATTR (WARNING,     COLOR_YELLOW,  -1); | ||||||
|  | 	INIT_ATTR (ERROR,       COLOR_RED,     -1); | ||||||
|  | 
 | ||||||
|  | 	INIT_ATTR (EXTERNAL,    COLOR_WHITE,   -1); | ||||||
|  | 	INIT_ATTR (TIMESTAMP,   COLOR_WHITE,   -1); | ||||||
|  | 	INIT_ATTR (HIGHLIGHT,   COLOR_BRIGHT (YELLOW), COLOR_MAGENTA, TEXT_BOLD); | ||||||
|  | 	INIT_ATTR (ACTION,      COLOR_RED,     -1); | ||||||
|  | 	INIT_ATTR (USERHOST,    COLOR_CYAN,    -1); | ||||||
|  | 	INIT_ATTR (JOIN,        COLOR_GREEN,   -1); | ||||||
|  | 	INIT_ATTR (PART,        COLOR_RED,     -1); | ||||||
|  | 
 | ||||||
|  | #undef INIT_ATTR | ||||||
|  | 
 | ||||||
|  | 	// This prevents formatters from obtaining an attribute printer function
 | ||||||
|  | 	if (!have_ti) | ||||||
|  | 	{ | ||||||
|  | 		g_terminal.stdout_is_tty = false; | ||||||
|  | 		g_terminal.stderr_is_tty = false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	g_log_message_real = log_message_attributed; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // --- Helpers -----------------------------------------------------------------
 | // --- Helpers -----------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
| static int | static int | ||||||
| @ -4413,7 +4473,7 @@ formatter_flush (struct formatter *self, FILE *stream, int flush_opts) | |||||||
| 	if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP)) | 	if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP)) | ||||||
| 		line = line_wrap (line, g_terminal.columns); | 		line = line_wrap (line, g_terminal.columns); | ||||||
| 
 | 
 | ||||||
| 	struct attr_printer state = ATTR_PRINTER_INIT (self->ctx, stream); | 	struct attr_printer state = ATTR_PRINTER_INIT (self->ctx->theme, stream); | ||||||
| 	struct line_char_attrs attrs = {};  // Won't compare equal to anything
 | 	struct line_char_attrs attrs = {};  // Won't compare equal to anything
 | ||||||
| 	LIST_FOR_EACH (struct line_char, c, line) | 	LIST_FOR_EACH (struct line_char, c, line) | ||||||
| 	{ | 	{ | ||||||
| @ -4425,10 +4485,10 @@ formatter_flush (struct formatter *self, FILE *stream, int flush_opts) | |||||||
| 			formatter_putc (NULL, stream); | 			formatter_putc (NULL, stream); | ||||||
| 
 | 
 | ||||||
| 			attrs = c->attrs; | 			attrs = c->attrs; | ||||||
| 			if (attrs.named != -1) | 			if (attrs.named == -1) | ||||||
| 				attr_printer_apply_named (&state, attrs.named); |  | ||||||
| 			else |  | ||||||
| 				attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); | 				attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); | ||||||
|  | 			else | ||||||
|  | 				attr_printer_apply_named (&state, attrs.named); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		formatter_putc (c, stream); | 		formatter_putc (c, stream); | ||||||
| @ -6820,17 +6880,28 @@ on_refresh_prompt (struct app_context *ctx) | |||||||
| 	char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL); | 	char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL); | ||||||
| 	str_free (&prompt); | 	str_free (&prompt); | ||||||
| 
 | 
 | ||||||
|  | 	// XXX: to be completely correct, we should use tputs, but we cannot
 | ||||||
| 	if (have_attributes) | 	if (have_attributes) | ||||||
| 	{ | 	{ | ||||||
| 		// XXX: to be completely correct, we should use tputs, but we cannot
 | 		char buf[16384] = ""; | ||||||
| 		input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c%s", | 		FILE *memfp = fmemopen (buf, sizeof buf - 1, "wb"); | ||||||
| 			INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT], | 		struct attr_printer state = { ctx->theme, memfp, false }; | ||||||
| 			INPUT_END_IGNORE, | 
 | ||||||
| 			localized, | 		fputc (INPUT_START_IGNORE, memfp); | ||||||
| 			INPUT_START_IGNORE, ctx->attrs[ATTR_RESET], | 		attr_printer_apply_named (&state, ATTR_PROMPT); | ||||||
| 			INPUT_END_IGNORE, | 		fputc (INPUT_END_IGNORE, memfp); | ||||||
| 			attributed_suffix)); | 
 | ||||||
|  | 		fputs (localized, memfp); | ||||||
| 		free (localized); | 		free (localized); | ||||||
|  | 
 | ||||||
|  | 		fputc (INPUT_START_IGNORE, memfp); | ||||||
|  | 		attr_printer_reset (&state); | ||||||
|  | 		fputc (INPUT_END_IGNORE, memfp); | ||||||
|  | 
 | ||||||
|  | 		fputs (attributed_suffix, memfp); | ||||||
|  | 
 | ||||||
|  | 		fclose (memfp); | ||||||
|  | 		input_maybe_set_prompt (ctx->input, xstrdup (buf)); | ||||||
| 	} | 	} | ||||||
| 	else | 	else | ||||||
| 		input_maybe_set_prompt (ctx->input, localized); | 		input_maybe_set_prompt (ctx->input, localized); | ||||||
|  | |||||||
							
								
								
									
										173
									
								
								xP/public/xP.js
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								xP/public/xP.js
									
									
									
									
									
								
							| @ -127,6 +127,32 @@ class RelayRpc extends EventTarget { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ---- Utilities --------------------------------------------------------------
 | ||||||
|  | 
 | ||||||
|  | // On macOS, the Alt/Option key transforms characters, which basically breaks
 | ||||||
|  | // all event.altKey shortcuts, so require combining them with Control as well
 | ||||||
|  | // on that system.
 | ||||||
|  | function hasShortcutModifiers(event) { | ||||||
|  | 	// This method of detection only works with Blink browsers, as of writing.
 | ||||||
|  | 	return event.altKey && !event.metaKey && | ||||||
|  | 		(navigator.userAgentData?.platform === 'macOS') === event.ctrlKey | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const audioContext = new AudioContext() | ||||||
|  | 
 | ||||||
|  | function beep() { | ||||||
|  | 	let gain = audioContext.createGain() | ||||||
|  | 	gain.gain.value = 0.5 | ||||||
|  | 	gain.connect(audioContext.destination) | ||||||
|  | 
 | ||||||
|  | 	let oscillator = audioContext.createOscillator() | ||||||
|  | 	oscillator.type = "triangle" | ||||||
|  | 	oscillator.frequency.value = 800 | ||||||
|  | 	oscillator.connect(gain) | ||||||
|  | 	oscillator.start(audioContext.currentTime) | ||||||
|  | 	oscillator.stop(audioContext.currentTime + 0.1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ---- Event processing -------------------------------------------------------
 | // ---- Event processing -------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
| let rpc = new RelayRpc(proxy) | let rpc = new RelayRpc(proxy) | ||||||
| @ -137,7 +163,7 @@ let bufferCurrent = undefined | |||||||
| let bufferLog = undefined | let bufferLog = undefined | ||||||
| let bufferAutoscroll = true | let bufferAutoscroll = true | ||||||
| 
 | 
 | ||||||
| function resetBufferStats(b) { | function bufferResetStats(b) { | ||||||
| 	b.newMessages = 0 | 	b.newMessages = 0 | ||||||
| 	b.newUnimportantMessages = 0 | 	b.newUnimportantMessages = 0 | ||||||
| 	b.highlighted = false | 	b.highlighted = false | ||||||
| @ -147,6 +173,29 @@ function bufferActivate(name) { | |||||||
| 	rpc.send({command: 'BufferActivate', bufferName: name}) | 	rpc.send({command: 'BufferActivate', bufferName: name}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function bufferToggleLog() { | ||||||
|  | 	if (bufferLog) { | ||||||
|  | 		setTimeout(() => | ||||||
|  | 			document.getElementById('input')?.focus()) | ||||||
|  | 
 | ||||||
|  | 		bufferLog = undefined | ||||||
|  | 		m.redraw() | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	let name = bufferCurrent | ||||||
|  | 	rpc.send({ | ||||||
|  | 		command: 'BufferLog', | ||||||
|  | 		bufferName: name, | ||||||
|  | 	}).then(resp => { | ||||||
|  | 		if (bufferCurrent !== name) | ||||||
|  | 			return | ||||||
|  | 
 | ||||||
|  | 		bufferLog = rpc.base64decode(resp.log) | ||||||
|  | 		m.redraw() | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| let connecting = true | let connecting = true | ||||||
| rpc.connect().then(result => { | rpc.connect().then(result => { | ||||||
| 	buffers.clear() | 	buffers.clear() | ||||||
| @ -174,8 +223,12 @@ rpc.addEventListener('Ping', event => { | |||||||
| rpc.addEventListener('BufferUpdate', event => { | rpc.addEventListener('BufferUpdate', event => { | ||||||
| 	let e = event.detail, b = buffers.get(e.bufferName) | 	let e = event.detail, b = buffers.get(e.bufferName) | ||||||
| 	if (b === undefined) { | 	if (b === undefined) { | ||||||
| 		buffers.set(e.bufferName, (b = {lines: []})) | 		buffers.set(e.bufferName, (b = { | ||||||
| 		resetBufferStats(b) | 			lines: [], | ||||||
|  | 			history: [], | ||||||
|  | 			historyAt: 0, | ||||||
|  | 		})) | ||||||
|  | 		bufferResetStats(b) | ||||||
| 	} | 	} | ||||||
| 	b.hideUnimportant = e.hideUnimportant | 	b.hideUnimportant = e.hideUnimportant | ||||||
| }) | }) | ||||||
| @ -206,7 +259,7 @@ rpc.addEventListener('BufferRemove', event => { | |||||||
| rpc.addEventListener('BufferActivate', event => { | rpc.addEventListener('BufferActivate', event => { | ||||||
| 	let old = buffers.get(bufferCurrent) | 	let old = buffers.get(bufferCurrent) | ||||||
| 	if (old !== undefined) | 	if (old !== undefined) | ||||||
| 		resetBufferStats(old) | 		bufferResetStats(old) | ||||||
| 
 | 
 | ||||||
| 	bufferLast = bufferCurrent | 	bufferLast = bufferCurrent | ||||||
| 	let e = event.detail, b = buffers.get(e.bufferName) | 	let e = event.detail, b = buffers.get(e.bufferName) | ||||||
| @ -219,13 +272,21 @@ rpc.addEventListener('BufferActivate', event => { | |||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	textarea.focus() | 	textarea.focus() | ||||||
| 	if (old !== undefined) | 	if (old !== undefined) { | ||||||
| 		old.input = textarea.value | 		old.input = textarea.value | ||||||
|  | 		old.inputStart = textarea.selectionStart | ||||||
|  | 		old.inputEnd = textarea.selectionEnd | ||||||
|  | 		old.inputDirection = textarea.selectionDirection | ||||||
|  | 		// Note that we effectively overwrite the newest line
 | ||||||
|  | 		// with the current textarea contents, and jump there.
 | ||||||
|  | 		old.historyAt = old.history.length | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	if (b !== undefined) |  | ||||||
| 		textarea.value = b.input || '' |  | ||||||
| 	else |  | ||||||
| 	textarea.value = '' | 	textarea.value = '' | ||||||
|  | 	if (b !== undefined && b.input !== undefined) { | ||||||
|  | 		textarea.value = b.input | ||||||
|  | 		textarea.setSelectionRange(b.inputStart, b.inputEnd, b.inputDirection) | ||||||
|  | 	} | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| rpc.addEventListener('BufferLine', event => { | rpc.addEventListener('BufferLine', event => { | ||||||
| @ -265,8 +326,11 @@ rpc.addEventListener('BufferLine', event => { | |||||||
| 
 | 
 | ||||||
| 	// TODO: Find some way of highlighting the tab in a browser.
 | 	// TODO: Find some way of highlighting the tab in a browser.
 | ||||||
| 	// TODO: Also highlight on unseen private messages, like xC does.
 | 	// TODO: Also highlight on unseen private messages, like xC does.
 | ||||||
| 	if (!visible && line.isHighlight) | 	if (line.isHighlight) { | ||||||
|  | 		beep() | ||||||
|  | 		if (!visible) | ||||||
| 			b.highlighted = true | 			b.highlighted = true | ||||||
|  | 	} | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| rpc.addEventListener('BufferClear', event => { | rpc.addEventListener('BufferClear', event => { | ||||||
| @ -307,26 +371,11 @@ let Toolbar = { | |||||||
| 		bufferAutoscroll = !bufferAutoscroll | 		bufferAutoscroll = !bufferAutoscroll | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	toggleLog: () => { |  | ||||||
| 		if (bufferLog) { |  | ||||||
| 			bufferLog = undefined |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		rpc.send({ |  | ||||||
| 			command: 'BufferLog', |  | ||||||
| 			bufferName: bufferCurrent, |  | ||||||
| 		}).then(resp => { |  | ||||||
| 			bufferLog = rpc.base64decode(resp.log) |  | ||||||
| 			m.redraw() |  | ||||||
| 		}) |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	view: vnode => { | 	view: vnode => { | ||||||
| 		return m('.toolbar', {}, [ | 		return m('.toolbar', {}, [ | ||||||
| 			m('button', {onclick: Toolbar.toggleAutoscroll}, | 			m('button', {onclick: Toolbar.toggleAutoscroll}, | ||||||
| 				bufferAutoscroll ? 'Scroll lock' : 'Scroll unlock'), | 				bufferAutoscroll ? 'Scroll lock' : 'Scroll unlock'), | ||||||
| 			m('button', {onclick: Toolbar.toggleLog}, | 			m('button', {onclick: event => bufferToggleLog()}, | ||||||
| 				bufferLog === undefined ? 'Show log' : 'Hide log'), | 				bufferLog === undefined ? 'Show log' : 'Hide log'), | ||||||
| 		]) | 		]) | ||||||
| 	}, | 	}, | ||||||
| @ -500,8 +549,8 @@ let Buffer = { | |||||||
| 
 | 
 | ||||||
| let Log = { | let Log = { | ||||||
| 	oncreate: vnode => { | 	oncreate: vnode => { | ||||||
| 		if (vnode.dom !== undefined) |  | ||||||
| 		vnode.dom.scrollTop = vnode.dom.scrollHeight | 		vnode.dom.scrollTop = vnode.dom.scrollHeight | ||||||
|  | 		vnode.dom.focus() | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	linkify: text => { | 	linkify: text => { | ||||||
| @ -571,28 +620,74 @@ let Input = { | |||||||
| 			bufferName: bufferCurrent, | 			bufferName: bufferCurrent, | ||||||
| 			text: textarea.value, | 			text: textarea.value, | ||||||
| 		}) | 		}) | ||||||
|  | 
 | ||||||
|  | 		// b.history[b.history.length] is virtual, and is represented
 | ||||||
|  | 		// either by textarea contents when it's currently being edited,
 | ||||||
|  | 		// or by b.input in all other cases.
 | ||||||
|  | 		let b = buffers.get(bufferCurrent) | ||||||
|  | 		b.history.push(textarea.value) | ||||||
|  | 		b.historyAt = b.history.length | ||||||
| 		textarea.value = '' | 		textarea.value = '' | ||||||
| 		return true | 		return true | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	previous: textarea => { | ||||||
|  | 		let b = buffers.get(bufferCurrent) | ||||||
|  | 		if (b === undefined) | ||||||
|  | 			return false | ||||||
|  | 
 | ||||||
|  | 		if (b.historyAt > 0) { | ||||||
|  | 			if (b.historyAt == b.history.length) | ||||||
|  | 				b.input = textarea.value | ||||||
|  | 			textarea.value = b.history[--b.historyAt] | ||||||
|  | 		} else { | ||||||
|  | 			beep() | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	next: textarea => { | ||||||
|  | 		let b = buffers.get(bufferCurrent) | ||||||
|  | 		if (b === undefined) | ||||||
|  | 			return false | ||||||
|  | 
 | ||||||
|  | 		if (b.historyAt < b.history.length) { | ||||||
|  | 			if (++b.historyAt == b.history.length) | ||||||
|  | 				textarea.value = b.input | ||||||
|  | 			else | ||||||
|  | 				textarea.value = b.history[b.historyAt] | ||||||
|  | 		} else { | ||||||
|  | 			beep() | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
| 	onKeyDown: event => { | 	onKeyDown: event => { | ||||||
| 		// TODO: And perhaps on other actions, too.
 | 		// TODO: And perhaps on other actions, too.
 | ||||||
| 		rpc.send({command: 'Active'}) | 		rpc.send({command: 'Active'}) | ||||||
| 
 | 
 | ||||||
| 		let textarea = event.currentTarget | 		let textarea = event.currentTarget | ||||||
| 		let handled = false | 		let handled = false | ||||||
|  | 		if (hasShortcutModifiers(event)) { | ||||||
|  | 			switch (event.key) { | ||||||
|  | 			case 'p': | ||||||
|  | 				handled = Input.previous(textarea) | ||||||
|  | 				break | ||||||
|  | 			case 'n': | ||||||
|  | 				handled = Input.next(textarea) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} else if (!event.altKey && !event.ctrlKey && !event.metaKey && | ||||||
|  | 				!event.shiftKey) { | ||||||
| 			switch (event.keyCode) { | 			switch (event.keyCode) { | ||||||
| 			case 9: | 			case 9: | ||||||
| 			if (!event.ctrlKey && !event.metaKey && !event.altKey && |  | ||||||
| 				!event.shiftKey) |  | ||||||
| 				handled = Input.complete(textarea) | 				handled = Input.complete(textarea) | ||||||
| 				break | 				break | ||||||
| 			case 13: | 			case 13: | ||||||
| 			if (!event.ctrlKey && !event.metaKey && !event.altKey && |  | ||||||
| 				!event.shiftKey) |  | ||||||
| 				handled = Input.submit(textarea) | 				handled = Input.submit(textarea) | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 		if (handled) | 		if (handled) | ||||||
| 			event.preventDefault() | 			event.preventDefault() | ||||||
| 	}, | 	}, | ||||||
| @ -623,26 +718,34 @@ let Main = { | |||||||
| window.addEventListener('load', () => m.mount(document.body, Main)) | window.addEventListener('load', () => m.mount(document.body, Main)) | ||||||
| 
 | 
 | ||||||
| document.addEventListener('keydown', event => { | document.addEventListener('keydown', event => { | ||||||
| 	if (rpc.ws == undefined || event.ctrlKey || event.metaKey) | 	if (rpc.ws == undefined || !hasShortcutModifiers(event)) | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	if (event.altKey && event.key == 'Tab') { | 	switch (event.key) { | ||||||
|  | 	case 'Tab': | ||||||
| 		if (bufferLast !== undefined) | 		if (bufferLast !== undefined) | ||||||
| 			bufferActivate(bufferLast) | 			bufferActivate(bufferLast) | ||||||
| 	} else if (event.altKey && event.key == 'a') { | 		break | ||||||
|  | 	case 'h': | ||||||
|  | 		bufferToggleLog() | ||||||
|  | 		break | ||||||
|  | 	case 'a': | ||||||
| 		for (const [name, b] of buffers) | 		for (const [name, b] of buffers) | ||||||
| 			if (name !== bufferCurrent && b.newMessages) { | 			if (name !== bufferCurrent && b.newMessages) { | ||||||
| 				bufferActivate(name) | 				bufferActivate(name) | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 	} else if (event.altKey && event.key == '!') { | 		break | ||||||
|  | 	case '!': | ||||||
| 		for (const [name, b] of buffers) | 		for (const [name, b] of buffers) | ||||||
| 			if (name !== bufferCurrent && b.highlighted) { | 			if (name !== bufferCurrent && b.highlighted) { | ||||||
| 				bufferActivate(name) | 				bufferActivate(name) | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 	} else | 		break | ||||||
|  | 	default: | ||||||
| 		return | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	event.preventDefault() | 	event.preventDefault() | ||||||
| }) | }) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user