Compare commits

..

No commits in common. "62773acaa099d7633a6d83d44b790f4f53fb274e" and "0bc2c12eecfb5b035c498272556f8fc6a39059a9" have entirely different histories.

5 changed files with 269 additions and 459 deletions

6
NEWS
View File

@ -1,4 +1,4 @@
2.0.0 (Unreleased)
Unreleased
* 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,
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: improved pager integration capabilities

View File

@ -26,13 +26,12 @@ As a unique bonus, you can launch a full text editor from within.
xP
--
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
--
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
with 'xP', and is hidden behind a CMake option.
It's currently in development, and hidden behind a CMake option.
xD
--
@ -118,19 +117,8 @@ as a `forking` type systemd user service.
xP
~~
The precondition for running 'xC' frontends is enabling its relay interface:
/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.
Install the Go compiler, and build the server using `make` in its directory,
then run it from within the _public_ subdirectory.
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.date_change_line = "%a %e %b %Y"
/set general.plugin_autoload += "fancy-prompt.lua"
/set theme.userhost = "109"
/set theme.join = "108"
/set theme.part = "138"
/set theme.external = "248"
/set theme.timestamp = "250 255"
/set theme.read_marker = "202"
/set attributes.userhost = "\x1b[38;5;109m"
/set attributes.join = "\x1b[38;5;108m"
/set attributes.part = "\x1b[38;5;138m"
/set attributes.external = "\x1b[38;5;248m"
/set attributes.timestamp = "\x1b[48;5;255m\x1b[38;5;250m"
/set attributes.read_marker = "\x1b[38;5;202m"
Configuration profiles
----------------------

@ -1 +1 @@
Subproject commit 22a121383f73fa7739f324021b6ad0ba6ed3cdb3
Subproject commit f545be725df9195a5b5897ad95a0220acf10f148

491
xC.c
View File

@ -19,6 +19,7 @@
// A table of all attributes we use for output
#define ATTR_TABLE(XX) \
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( READ_MARKER, read_marker, Terminal attrs for the read marker ) \
XX( WARNING, warning, Terminal attrs for warnings ) \
@ -33,7 +34,6 @@
enum
{
ATTR_RESET,
#define XX(x, y, z) ATTR_ ## x,
ATTR_TABLE (XX)
#undef XX
@ -48,9 +48,6 @@ enum
#include "config.h"
#define PROGRAM_NAME "xC"
// fmemopen
#define _POSIX_C_SOURCE 200809L
#include "common.c"
#include "xD-replies.c"
#include "xC-proto.c"
@ -1455,58 +1452,6 @@ channel_destroy (struct channel *self)
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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enum formatter_item_type
@ -1520,6 +1465,17 @@ enum formatter_item_type
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
{
enum formatter_item_type type : 16; ///< Type of this item
@ -2160,13 +2116,12 @@ struct completion_hook
struct app_context
{
/// Default terminal attributes
struct attrs theme_defaults[ATTR_COUNT];
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
// 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 beep_on_highlight; ///< Beep on highlight
bool logging; ///< Logging to file enabled
@ -2350,6 +2305,11 @@ app_context_free (struct app_context *self)
plugin_destroy (iter);
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)
{
@ -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_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);
#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, \
.on_change = on_config_theme_change },
.on_change = on_config_attribute_change },
ATTR_TABLE (XX)
#undef XX
{}
@ -2706,9 +2666,9 @@ load_config_general (struct config_item *subtree, void *user_data)
}
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
@ -2720,7 +2680,7 @@ register_config_modules (struct app_context *ctx)
config_register_module (config, "aliases", NULL, NULL);
config_register_module (config, "plugins", NULL, NULL);
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);
}
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
relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
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++)
len++;
// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
union relay_item_data *p = e->items = xcalloc (len * 9, sizeof *e->items);
// XXX: This way helps xP's JSON conversion, but is super annoying for us.
union relay_item_data *p = e->items = xcalloc (len * 6, sizeof *e->items);
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;
}
@ -3298,6 +3239,123 @@ get_attribute_printer (FILE *stream)
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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 attrs *attrs; ///< Named attributes
char **attrs; ///< Named attributes
FILE *stream; ///< Output stream
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
attr_printer_filtered_puts (FILE *stream, const char *attr)
@ -3350,11 +3408,22 @@ static void
attr_printer_reset (struct attr_printer *self)
{
if (self->dirty)
attr_printer_tputs (self, exit_attribute_mode);
attr_printer_tputs (self, self->attrs[ATTR_RESET]);
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:
// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
// 16 colours (okayish, we have the full basic range guaranteed)
@ -3482,135 +3551,6 @@ attr_printer_apply (struct attr_printer *self,
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 -----------------------------------------------------------------
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))
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
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);
attrs = c->attrs;
if (attrs.named == -1)
attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg);
else
if (attrs.named != -1)
attr_printer_apply_named (&state, attrs.named);
else
attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg);
}
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);
str_free (&prompt);
// XXX: to be completely correct, we should use tputs, but we cannot
if (have_attributes)
{
char buf[16384] = "";
FILE *memfp = fmemopen (buf, sizeof buf - 1, "wb");
struct attr_printer state = { ctx->theme, memfp, false };
fputc (INPUT_START_IGNORE, memfp);
attr_printer_apply_named (&state, ATTR_PROMPT);
fputc (INPUT_END_IGNORE, memfp);
fputs (localized, memfp);
// XXX: to be completely correct, we should use tputs, but we cannot
input_maybe_set_prompt (ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c%s",
INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT],
INPUT_END_IGNORE,
localized,
INPUT_START_IGNORE, ctx->attrs[ATTR_RESET],
INPUT_END_IGNORE,
attributed_suffix));
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
input_maybe_set_prompt (ctx->input, localized);

View File

@ -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 -------------------------------------------------------
let rpc = new RelayRpc(proxy)
@ -163,7 +137,7 @@ let bufferCurrent = undefined
let bufferLog = undefined
let bufferAutoscroll = true
function bufferResetStats(b) {
function resetBufferStats(b) {
b.newMessages = 0
b.newUnimportantMessages = 0
b.highlighted = false
@ -173,29 +147,6 @@ function bufferActivate(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
rpc.connect().then(result => {
buffers.clear()
@ -223,12 +174,8 @@ rpc.addEventListener('Ping', event => {
rpc.addEventListener('BufferUpdate', event => {
let e = event.detail, b = buffers.get(e.bufferName)
if (b === undefined) {
buffers.set(e.bufferName, (b = {
lines: [],
history: [],
historyAt: 0,
}))
bufferResetStats(b)
buffers.set(e.bufferName, (b = {lines: []}))
resetBufferStats(b)
}
b.hideUnimportant = e.hideUnimportant
})
@ -259,7 +206,7 @@ rpc.addEventListener('BufferRemove', event => {
rpc.addEventListener('BufferActivate', event => {
let old = buffers.get(bufferCurrent)
if (old !== undefined)
bufferResetStats(old)
resetBufferStats(old)
bufferLast = bufferCurrent
let e = event.detail, b = buffers.get(e.bufferName)
@ -272,21 +219,13 @@ rpc.addEventListener('BufferActivate', event => {
return
textarea.focus()
if (old !== undefined) {
if (old !== undefined)
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 = ''
if (b !== undefined && b.input !== undefined) {
textarea.value = b.input
textarea.setSelectionRange(b.inputStart, b.inputEnd, b.inputDirection)
}
})
rpc.addEventListener('BufferLine', event => {
@ -326,11 +265,8 @@ rpc.addEventListener('BufferLine', event => {
// TODO: Find some way of highlighting the tab in a browser.
// TODO: Also highlight on unseen private messages, like xC does.
if (line.isHighlight) {
beep()
if (!visible)
if (!visible && line.isHighlight)
b.highlighted = true
}
})
rpc.addEventListener('BufferClear', event => {
@ -371,11 +307,26 @@ let Toolbar = {
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 => {
return m('.toolbar', {}, [
m('button', {onclick: Toolbar.toggleAutoscroll},
bufferAutoscroll ? 'Scroll lock' : 'Scroll unlock'),
m('button', {onclick: event => bufferToggleLog()},
m('button', {onclick: Toolbar.toggleLog},
bufferLog === undefined ? 'Show log' : 'Hide log'),
])
},
@ -549,8 +500,8 @@ let Buffer = {
let Log = {
oncreate: vnode => {
if (vnode.dom !== undefined)
vnode.dom.scrollTop = vnode.dom.scrollHeight
vnode.dom.focus()
},
linkify: text => {
@ -620,74 +571,28 @@ let Input = {
bufferName: bufferCurrent,
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 = ''
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 => {
// TODO: And perhaps on other actions, too.
rpc.send({command: 'Active'})
let textarea = event.currentTarget
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) {
case 9:
if (!event.ctrlKey && !event.metaKey && !event.altKey &&
!event.shiftKey)
handled = Input.complete(textarea)
break
case 13:
if (!event.ctrlKey && !event.metaKey && !event.altKey &&
!event.shiftKey)
handled = Input.submit(textarea)
break
}
}
if (handled)
event.preventDefault()
},
@ -718,34 +623,26 @@ let Main = {
window.addEventListener('load', () => m.mount(document.body, Main))
document.addEventListener('keydown', event => {
if (rpc.ws == undefined || !hasShortcutModifiers(event))
if (rpc.ws == undefined || event.ctrlKey || event.metaKey)
return
switch (event.key) {
case 'Tab':
if (event.altKey && event.key == 'Tab') {
if (bufferLast !== undefined)
bufferActivate(bufferLast)
break
case 'h':
bufferToggleLog()
break
case 'a':
} else if (event.altKey && event.key == 'a') {
for (const [name, b] of buffers)
if (name !== bufferCurrent && b.newMessages) {
bufferActivate(name)
break
}
break
case '!':
} else if (event.altKey && event.key == '!') {
for (const [name, b] of buffers)
if (name !== bufferCurrent && b.highlighted) {
bufferActivate(name)
break
}
break
default:
} else
return
}
event.preventDefault()
})