Compare commits
	
		
			No commits in common. "62773acaa099d7633a6d83d44b790f4f53fb274e" and "0bc2c12eecfb5b035c498272556f8fc6a39059a9" have entirely different histories.
		
	
	
		
			62773acaa0
			...
			0bc2c12eec
		
	
		
							
								
								
									
										6
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								NEWS
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| 2.0.0 (Unreleased) | Unreleased | ||||||
| 
 | 
 | ||||||
|  * xD: implemented WALLOPS, choosing to make it target even non-operators |  * xD: implemented WALLOPS, choosing to make it target even non-operators | ||||||
| 
 | 
 | ||||||
| @ -8,10 +8,6 @@ | |||||||
|    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,13 +26,12 @@ 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 a bit rough around the edges, yet fully usable. | So far it's somewhat basic, yet 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. | ||||||
| This subproject has been put on hold, partly because of its massive overlap | It's currently in development, and hidden behind a CMake option. | ||||||
| with 'xP', and is hidden behind a CMake option. |  | ||||||
| 
 | 
 | ||||||
| xD | xD | ||||||
| -- | -- | ||||||
| @ -118,19 +117,8 @@ as a `forking` type systemd user service. | |||||||
| 
 | 
 | ||||||
| xP | xP | ||||||
| ~~ | ~~ | ||||||
| The precondition for running 'xC' frontends is enabling its relay interface: | Install the Go compiler, and build the server using `make` in its directory, | ||||||
| 
 | 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 | ||||||
| ------------------- | ------------------- | ||||||
| @ -177,12 +165,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 theme.userhost = "109" |  /set attributes.userhost = "\x1b[38;5;109m" | ||||||
|  /set theme.join = "108" |  /set attributes.join = "\x1b[38;5;108m" | ||||||
|  /set theme.part = "138" |  /set attributes.part = "\x1b[38;5;138m" | ||||||
|  /set theme.external = "248" |  /set attributes.external = "\x1b[38;5;248m" | ||||||
|  /set theme.timestamp = "250 255" |  /set attributes.timestamp = "\x1b[48;5;255m\x1b[38;5;250m" | ||||||
|  /set theme.read_marker = "202" |  /set attributes.read_marker = "\x1b[38;5;202m" | ||||||
| 
 | 
 | ||||||
| Configuration profiles | Configuration profiles | ||||||
| ---------------------- | ---------------------- | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								liberty
									
									
									
									
									
								
							| @ -1 +1 @@ | |||||||
| Subproject commit 22a121383f73fa7739f324021b6ad0ba6ed3cdb3 | Subproject commit f545be725df9195a5b5897ad95a0220acf10f148 | ||||||
							
								
								
									
										491
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										491
									
								
								xC.c
									
									
									
									
									
								
							| @ -19,6 +19,7 @@ | |||||||
| // 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         ) \ | ||||||
| @ -33,7 +34,6 @@ | |||||||
| 
 | 
 | ||||||
| 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,9 +48,6 @@ 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" | ||||||
| @ -1455,58 +1452,6 @@ 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 | ||||||
| @ -1520,6 +1465,17 @@ 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
 | ||||||
| @ -2160,13 +2116,12 @@ struct completion_hook | |||||||
| 
 | 
 | ||||||
| struct app_context | struct app_context | ||||||
| { | { | ||||||
| 	/// Default terminal attributes
 | 	char *attrs_defaults[ATTR_COUNT];   ///< Default terminal attributes
 | ||||||
| 	struct attrs theme_defaults[ATTR_COUNT]; |  | ||||||
| 
 | 
 | ||||||
| 	// Configuration:
 | 	// Configuration:
 | ||||||
| 
 | 
 | ||||||
| 	struct config config;               ///< Program configuration
 | 	struct config config;               ///< Program configuration
 | ||||||
| 	struct attrs theme[ATTR_COUNT];     ///< Terminal attributes
 | 	char *attrs[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
 | ||||||
| @ -2350,6 +2305,11 @@ 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) | ||||||
| 	{ | 	{ | ||||||
| @ -2407,7 +2367,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_theme_change (struct config_item *item); | static void on_config_attribute_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)                                        \ | ||||||
| @ -2688,10 +2648,10 @@ static struct config_schema g_config_general[] = | |||||||
| 	{} | 	{} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static struct config_schema g_config_theme[] = | static struct config_schema g_config_attributes[] = | ||||||
| { | { | ||||||
| #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_theme_change }, | 	.on_change = on_config_attribute_change }, | ||||||
| 	ATTR_TABLE (XX) | 	ATTR_TABLE (XX) | ||||||
| #undef XX | #undef XX | ||||||
| 	{} | 	{} | ||||||
| @ -2706,9 +2666,9 @@ load_config_general (struct config_item *subtree, void *user_data) | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| load_config_theme (struct config_item *subtree, void *user_data) | load_config_attributes (struct config_item *subtree, void *user_data) | ||||||
| { | { | ||||||
| 	config_schema_apply_to_object (g_config_theme, subtree, user_data); | 	config_schema_apply_to_object (g_config_attributes, subtree, user_data); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static void | static void | ||||||
| @ -2720,7 +2680,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, "theme",   load_config_theme,   ctx); | 	config_register_module (config, "attributes", load_config_attributes, ctx); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | ||||||
| @ -3154,64 +3114,6 @@ 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) | ||||||
| @ -3230,10 +3132,49 @@ 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++; | ||||||
| 
 | 
 | ||||||
| 	// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
 | 	// XXX: This way helps xP's JSON conversion, but is super annoying for us.
 | ||||||
| 	union relay_item_data *p = e->items = xcalloc (len * 9, sizeof *e->items); | 	union relay_item_data *p = e->items = xcalloc (len * 6, 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; | ||||||
| } | } | ||||||
| @ -3298,6 +3239,123 @@ 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
 | ||||||
| @ -3307,12 +3365,12 @@ get_attribute_printer (FILE *stream) | |||||||
| 
 | 
 | ||||||
| struct attr_printer | struct attr_printer | ||||||
| { | { | ||||||
| 	struct attrs *attrs;                ///< Named attributes
 | 	char **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(attrs, stream) { attrs, stream, true } | #define ATTR_PRINTER_INIT(ctx, stream) { ctx->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) | ||||||
| @ -3350,11 +3408,22 @@ 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, exit_attribute_mode); | 		attr_printer_tputs (self, self->attrs[ATTR_RESET]); | ||||||
| 
 | 
 | ||||||
| 	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)
 | ||||||
| @ -3482,135 +3551,6 @@ 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 | ||||||
| @ -4473,7 +4413,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->theme, stream); | 	struct attr_printer state = ATTR_PRINTER_INIT (self->ctx, 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) | ||||||
| 	{ | 	{ | ||||||
| @ -4485,10 +4425,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 (&state, attrs.text, attrs.fg, attrs.bg); |  | ||||||
| 			else |  | ||||||
| 				attr_printer_apply_named (&state, attrs.named); | 				attr_printer_apply_named (&state, attrs.named); | ||||||
|  | 			else | ||||||
|  | 				attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		formatter_putc (c, stream); | 		formatter_putc (c, stream); | ||||||
| @ -6880,28 +6820,17 @@ 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) | ||||||
| 	{ | 	{ | ||||||
| 		char buf[16384] = ""; | 		// XXX: to be completely correct, we should use tputs, but we cannot
 | ||||||
| 		FILE *memfp = fmemopen (buf, sizeof buf - 1, "wb"); | 		input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c%s", | ||||||
| 		struct attr_printer state = { ctx->theme, memfp, false }; | 			INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT], | ||||||
| 
 | 			INPUT_END_IGNORE, | ||||||
| 		fputc (INPUT_START_IGNORE, memfp); | 			localized, | ||||||
| 		attr_printer_apply_named (&state, ATTR_PROMPT); | 			INPUT_START_IGNORE, ctx->attrs[ATTR_RESET], | ||||||
| 		fputc (INPUT_END_IGNORE, memfp); | 			INPUT_END_IGNORE, | ||||||
| 
 | 			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,32 +127,6 @@ 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) | ||||||
| @ -163,7 +137,7 @@ let bufferCurrent = undefined | |||||||
| let bufferLog = undefined | let bufferLog = undefined | ||||||
| let bufferAutoscroll = true | let bufferAutoscroll = true | ||||||
| 
 | 
 | ||||||
| function bufferResetStats(b) { | function resetBufferStats(b) { | ||||||
| 	b.newMessages = 0 | 	b.newMessages = 0 | ||||||
| 	b.newUnimportantMessages = 0 | 	b.newUnimportantMessages = 0 | ||||||
| 	b.highlighted = false | 	b.highlighted = false | ||||||
| @ -173,29 +147,6 @@ 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() | ||||||
| @ -223,12 +174,8 @@ 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 = { | 		buffers.set(e.bufferName, (b = {lines: []})) | ||||||
| 			lines: [], | 		resetBufferStats(b) | ||||||
| 			history: [], |  | ||||||
| 			historyAt: 0, |  | ||||||
| 		})) |  | ||||||
| 		bufferResetStats(b) |  | ||||||
| 	} | 	} | ||||||
| 	b.hideUnimportant = e.hideUnimportant | 	b.hideUnimportant = e.hideUnimportant | ||||||
| }) | }) | ||||||
| @ -259,7 +206,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) | ||||||
| 		bufferResetStats(old) | 		resetBufferStats(old) | ||||||
| 
 | 
 | ||||||
| 	bufferLast = bufferCurrent | 	bufferLast = bufferCurrent | ||||||
| 	let e = event.detail, b = buffers.get(e.bufferName) | 	let e = event.detail, b = buffers.get(e.bufferName) | ||||||
| @ -272,21 +219,13 @@ 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 => { | ||||||
| @ -326,11 +265,8 @@ 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 (line.isHighlight) { | 	if (!visible && line.isHighlight) | ||||||
| 		beep() |  | ||||||
| 		if (!visible) |  | ||||||
| 		b.highlighted = true | 		b.highlighted = true | ||||||
| 	} |  | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| rpc.addEventListener('BufferClear', event => { | rpc.addEventListener('BufferClear', event => { | ||||||
| @ -371,11 +307,26 @@ 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: event => bufferToggleLog()}, | 			m('button', {onclick: Toolbar.toggleLog}, | ||||||
| 				bufferLog === undefined ? 'Show log' : 'Hide log'), | 				bufferLog === undefined ? 'Show log' : 'Hide log'), | ||||||
| 		]) | 		]) | ||||||
| 	}, | 	}, | ||||||
| @ -549,8 +500,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 => { | ||||||
| @ -620,74 +571,28 @@ 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() | ||||||
| 	}, | 	}, | ||||||
| @ -718,34 +623,26 @@ 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 || !hasShortcutModifiers(event)) | 	if (rpc.ws == undefined || event.ctrlKey || event.metaKey) | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	switch (event.key) { | 	if (event.altKey && event.key == 'Tab') { | ||||||
| 	case 'Tab': |  | ||||||
| 		if (bufferLast !== undefined) | 		if (bufferLast !== undefined) | ||||||
| 			bufferActivate(bufferLast) | 			bufferActivate(bufferLast) | ||||||
| 		break | 	} else if (event.altKey && event.key == 'a') { | ||||||
| 	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 | ||||||
| 			} | 			} | ||||||
| 		break | 	} else if (event.altKey && event.key == '!') { | ||||||
| 	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 | ||||||
| 			} | 			} | ||||||
| 		break | 	} else | ||||||
| 	default: |  | ||||||
| 		return | 		return | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	event.preventDefault() | 	event.preventDefault() | ||||||
| }) | }) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user