Compare commits

..

15 Commits

Author SHA1 Message Date
0bc2c12eec
xP: handle the M-Tab binding from xC 2022-09-10 19:36:49 +02:00
3330683ad6
xP: handle M-a and M-! bindings from xC 2022-09-10 19:34:01 +02:00
0015d26dc8
xC/xP: support hiding unimportant messages at all 2022-09-10 19:01:42 +02:00
7d5e63be1f
xC: deal with so far unexpected multiline messages
And get rid of an outdated unmarked TODO comment.
2022-09-10 18:51:27 +02:00
e7d0f2380e
xC: split Command.BUFFER_INPUT on newlines 2022-09-10 18:51:27 +02:00
36529a46fd
xP: also scroll to bottom on window resize 2022-09-10 18:10:08 +02:00
632ac992ab
xC/xP: only send buffer stats in the initial sync
The client and frontends track these separately,
there is no need for hard synchronization.
2022-09-10 17:38:33 +02:00
d29e2cbfe8
xP: detect links in the log 2022-09-10 17:18:22 +02:00
240fac4d90
xP: only allow vertical textarea resizing 2022-09-10 17:08:14 +02:00
c06894b291
xP: fix command sequence number generation 2022-09-10 17:05:39 +02:00
9eaf78f823
xP: open links in a new tab/window 2022-09-10 17:05:39 +02:00
5f02dddd11
xP: advance unread marker when the log is visible 2022-09-10 17:05:39 +02:00
6f4a3f4657
xP: advance unread marker in an inactive tab 2022-09-10 17:05:39 +02:00
6387145adc
xP: improve line wrapping 2022-09-10 17:05:38 +02:00
f3cc137342
xC-gen-proto: reduce enums to single bytes
That's already way more than we can possibly use.
2022-09-10 16:06:35 +02:00
7 changed files with 191 additions and 95 deletions

View File

@ -110,11 +110,11 @@ function codegen_enum(name, cg, ctype) {
# XXX: This should also check if it isn't out-of-range for any reason, # XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit. # but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tstr_pack_i32(w, %s);\n" CodegenSerialize[name] = "\tstr_pack_i8(w, %s);\n"
CodegenDeserialize[name] = \ CodegenDeserialize[name] = \
"\t{\n" \ "\t{\n" \
"\t\tint32_t v = 0;\n" \ "\t\tint8_t v = 0;\n" \
"\t\tif (!msg_unpacker_i32(r, &v) || !v)\n" \ "\t\tif (!msg_unpacker_i8(r, &v) || !v)\n" \
"\t\t\treturn false;\n" \ "\t\t\treturn false;\n" \
"\t\t%s = v;\n" \ "\t\t%s = v;\n" \
"\t}\n" "\t}\n"

View File

@ -161,7 +161,7 @@ function codegen_begin() {
print "\tvar n int64" print "\tvar n int64"
print "\tif err := json.Unmarshal(data, &n); err != nil {" print "\tif err := json.Unmarshal(data, &n); err != nil {"
print "\t\treturn 0, err" print "\t\treturn 0, err"
print "\t} else if n > math.MaxInt32 || n < math.MinInt32 {" print "\t} else if n > math.MaxInt8 || n < math.MinInt8 {"
print "\t\treturn 0, errors.New(`integer out of range`)" print "\t\treturn 0, errors.New(`integer out of range`)"
print "\t} else {" print "\t} else {"
print "\t\treturn n, nil" print "\t\treturn n, nil"
@ -191,7 +191,7 @@ function codegen_enum_value(name, subname, value, cg, goname) {
function codegen_enum(name, cg, gotype, fields) { function codegen_enum(name, cg, gotype, fields) {
gotype = PrefixCamel name gotype = PrefixCamel name
print "type " gotype " int" print "type " gotype " int8"
print "" print ""
print "const (" print "const ("
@ -239,12 +239,10 @@ function codegen_enum(name, cg, gotype, fields) {
# XXX: This should also check if it isn't out-of-range for any reason, # XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit. # but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = \ CodegenSerialize[name] = "\tdata = append(data, uint8(%s))\n"
"\tdata = binary.BigEndian.AppendUint32(data, uint32(%s))\n"
CodegenDeserialize[name] = \ CodegenDeserialize[name] = \
"\tif len(data) >= 4 {\n" \ "\tif len(data) >= 1 {\n" \
"\t\t%s = " gotype "(int32(binary.BigEndian.Uint32(data)))\n" \ "\t\t%s, data = " gotype "(data[0]), data[1:]\n" \
"\t\tdata = data[4:]\n" \
"\t} else {\n" \ "\t} else {\n" \
"\t\treturn nil, false\n" \ "\t\treturn nil, false\n" \
"\t}\n" "\t}\n"

View File

@ -15,7 +15,7 @@
# Booleans are one byte each. # Booleans are one byte each.
# Strings must be valid UTF-8, use u8<> to lift that restriction. # Strings must be valid UTF-8, use u8<> to lift that restriction.
# String and array lengths are encoded as u32. # String and array lengths are encoded as u32.
# Enumeration values automatically start at 1, and are encoded as i32. # Enumeration values automatically start at 1, and are encoded as i8.
# Any struct or union field may be a variable-length array. # Any struct or union field may be a variable-length array.
# #
# Message framing is done externally, but also happens to prefix u32 lengths. # Message framing is done externally, but also happens to prefix u32 lengths.
@ -189,6 +189,8 @@ function defenum( name, ident, value, cg) {
value = readnumber() value = readnumber()
if (!value) if (!value)
fatal("enumeration values cannot be zero") fatal("enumeration values cannot be zero")
if (value < -128 || value > 127)
fatal("enumeration value out of range")
expect(accept(",")) expect(accept(","))
append(EnumValues, name, SUBSEP ident) append(EnumValues, name, SUBSEP ident)
if (EnumValues[name, ident]++) if (EnumValues[name, ident]++)

View File

@ -19,7 +19,7 @@ struct CommandMessage {
case HELLO: case HELLO:
u32 version; u32 version;
// If the version check succeeds, the client will receive // If the version check succeeds, the client will receive
// an initial stream of BUFFER_UPDATE, BUFFER_LINE, // an initial stream of BUFFER_UPDATE, BUFFER_STATS, BUFFER_LINE,
// and finally a BUFFER_ACTIVATE message. // and finally a BUFFER_ACTIVATE message.
case ACTIVE: case ACTIVE:
void; void;
@ -50,6 +50,7 @@ struct EventMessage {
union EventData switch (enum Event { union EventData switch (enum Event {
PING, PING,
BUFFER_UPDATE, BUFFER_UPDATE,
BUFFER_STATS,
BUFFER_RENAME, BUFFER_RENAME,
BUFFER_REMOVE, BUFFER_REMOVE,
BUFFER_ACTIVATE, BUFFER_ACTIVATE,
@ -61,13 +62,15 @@ struct EventMessage {
case PING: case PING:
void; void;
case BUFFER_UPDATE: case BUFFER_UPDATE:
string buffer_name;
bool hide_unimportant;
case BUFFER_STATS:
string buffer_name; string buffer_name;
// These are cumulative, even for lines flushed out from buffers. // These are cumulative, even for lines flushed out from buffers.
// Updates to these values aren't broadcasted, thus handle: // Updates to these values aren't broadcasted, thus handle:
// - BUFFER_LINE by bumping/setting them as appropriate, // - BUFFER_LINE by bumping/setting them as appropriate,
// - BUFFER_ACTIVATE by clearing them for the previous buffer // - BUFFER_ACTIVATE by clearing them for the previous buffer
// (this way, they can be used to mark unread messages). // (this way, they can be used to mark unread messages).
// Any updates received after the initial sync should be ignored.
u32 new_messages; u32 new_messages;
u32 new_unimportant_messages; u32 new_unimportant_messages;
bool highlighted; bool highlighted;

83
xC.c
View File

@ -3068,6 +3068,16 @@ relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
struct relay_event_data_buffer_update *e = &m->data.buffer_update; struct relay_event_data_buffer_update *e = &m->data.buffer_update;
e->event = RELAY_EVENT_BUFFER_UPDATE; e->event = RELAY_EVENT_BUFFER_UPDATE;
e->buffer_name = str_from_cstr (buffer->name); e->buffer_name = str_from_cstr (buffer->name);
e->hide_unimportant = buffer->hide_unimportant;
}
static void
relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer)
{
struct relay_event_message *m = relay_prepare (ctx);
struct relay_event_data_buffer_stats *e = &m->data.buffer_stats;
e->event = RELAY_EVENT_BUFFER_STATS;
e->buffer_name = str_from_cstr (buffer->name);
e->new_messages = MIN (UINT32_MAX, e->new_messages = MIN (UINT32_MAX,
buffer->new_messages_count - buffer->new_unimportant_count); buffer->new_messages_count - buffer->new_unimportant_count);
e->new_unimportant_messages = MIN (UINT32_MAX, e->new_unimportant_messages = MIN (UINT32_MAX,
@ -8171,7 +8181,7 @@ irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
strv_free (&v); strv_free (&v);
} }
static bool process_input_utf8 static bool process_input_line
(struct app_context *, struct buffer *, const char *, int); (struct app_context *, struct buffer *, const char *, int);
static void on_autoaway_timer (struct app_context *ctx); static void on_autoaway_timer (struct app_context *ctx);
@ -8200,7 +8210,7 @@ irc_on_registered (struct server *s, const char *nickname)
if (command) if (command)
{ {
log_server_debug (s, "Executing \"#s\"", command); log_server_debug (s, "Executing \"#s\"", command);
(void) process_input_utf8 (s->ctx, s->buffer, command, 0); (void) process_input_line (s->ctx, s->buffer, command, 0);
} }
int64_t command_delay = get_config_integer (s->config, "command_delay"); int64_t command_delay = get_config_integer (s->config, "command_delay");
@ -9066,12 +9076,21 @@ irc_autosplit_message (struct server *s, const char *message,
- 1 - (int) strlen (s->irc_user_host) - 1 - (int) strlen (s->irc_user_host)
- 1 - fixed_part; - 1 - fixed_part;
// However we don't always have the full info for message splitting // Multiline messages can be triggered through hooks and plugins.
struct strv lines = strv_make ();
cstr_split (message, "\r\n", false, &lines);
bool success = true;
for (size_t i = 0; i < lines.len; i++)
{
// We don't always have the full info for message splitting.
if (!space_in_one_message) if (!space_in_one_message)
strv_append (output, message); strv_append (output, lines.vector[i]);
else if (!wrap_message (message, space_in_one_message, output, e)) else if (!(success =
return false; wrap_message (lines.vector[i], space_in_one_message, output, e)))
return true; break;
}
strv_free (&lines);
return success;
} }
static void static void
@ -9080,12 +9099,11 @@ send_autosplit_message (struct server *s,
const char *prefix, const char *suffix) const char *prefix, const char *suffix)
{ {
struct buffer *buffer = str_map_find (&s->irc_buffer_map, target); struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
// "COMMAND target * :prefix*suffix"
int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1 int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
+ strlen (prefix) + strlen (suffix); + strlen (prefix) + strlen (suffix);
// We might also want to preserve attributes across splits but
// that would make this code a lot more complicated
struct strv lines = strv_make (); struct strv lines = strv_make ();
struct error *e = NULL; struct error *e = NULL;
if (!irc_autosplit_message (s, message, fixed_part, &lines, &e)) if (!irc_autosplit_message (s, message, fixed_part, &lines, &e))
@ -9831,7 +9849,7 @@ lua_buffer_execute (lua_State *L)
struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info); struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
struct buffer *buffer = wrapper->object; struct buffer *buffer = wrapper->object;
const char *line = lua_plugin_check_utf8 (L, 2); const char *line = lua_plugin_check_utf8 (L, 2);
(void) process_input_utf8 (wrapper->plugin->ctx, buffer, line, 0); (void) process_input_line (wrapper->plugin->ctx, buffer, line, 0);
return 0; return 0;
} }
@ -13261,13 +13279,13 @@ process_alias (struct app_context *ctx, struct buffer *buffer,
log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"", log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"",
(int) i, commands->vector[i]); (int) i, commands->vector[i]);
for (size_t i = 0; i < commands->len; i++) for (size_t i = 0; i < commands->len; i++)
if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level)) if (!process_input_line (ctx, buffer, commands->vector[i], ++level))
return false; return false;
return true; return true;
} }
static bool static bool
process_input_utf8_posthook (struct app_context *ctx, struct buffer *buffer, process_input_line_posthook (struct app_context *ctx, struct buffer *buffer,
char *input, int alias_level) char *input, int alias_level)
{ {
if (*input != '/' || *++input == '/') if (*input != '/' || *++input == '/')
@ -13316,35 +13334,27 @@ process_input_hooks (struct app_context *ctx, struct buffer *buffer,
} }
static bool static bool
process_input_utf8 (struct app_context *ctx, struct buffer *buffer, process_input_line (struct app_context *ctx, struct buffer *buffer,
const char *input, int alias_level) const char *input, int alias_level)
{ {
// Note that this also gets called on expanded aliases, // Note that this also gets called on expanded aliases,
// which might or might not be desirable (we can forward "alias_level") // which might or might not be desirable (we can forward "alias_level")
char *processed = process_input_hooks (ctx, buffer, xstrdup (input)); char *processed = process_input_hooks (ctx, buffer, xstrdup (input));
bool result = !processed bool result = !processed
|| process_input_utf8_posthook (ctx, buffer, processed, alias_level); || process_input_line_posthook (ctx, buffer, processed, alias_level);
free (processed); free (processed);
return result; return result;
} }
static void static void
process_input (struct app_context *ctx, char *user_input) process_input (struct app_context *ctx, struct buffer *buffer,
const char *input)
{ {
char *input;
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
print_error ("character conversion failed for: %s", "user input");
else
{
struct strv lines = strv_make (); struct strv lines = strv_make ();
cstr_split (input, "\r\n", false, &lines); cstr_split (input, "\r\n", false, &lines);
for (size_t i = 0; i < lines.len; i++) for (size_t i = 0; i < lines.len; i++)
(void) process_input_utf8 (ctx, (void) process_input_line (ctx, buffer, lines.vector[i], 0);
ctx->current_buffer, lines.vector[i], 0);
strv_free (&lines); strv_free (&lines);
}
free (input);
} }
// --- Word completion --------------------------------------------------------- // --- Word completion ---------------------------------------------------------
@ -14193,6 +14203,10 @@ on_toggle_unimportant (int count, int key, void *user_data)
(void) key; (void) key;
struct app_context *ctx = user_data; struct app_context *ctx = user_data;
ctx->current_buffer->hide_unimportant ^= true; ctx->current_buffer->hide_unimportant ^= true;
relay_prepare_buffer_update (ctx, ctx->current_buffer);
relay_broadcast (ctx);
buffer_print_backlog (ctx, ctx->current_buffer); buffer_print_backlog (ctx, ctx->current_buffer);
return true; return true;
} }
@ -14580,7 +14594,7 @@ on_editline_return (EditLine *editline, int key)
} }
free (line); free (line);
// process_input() expects a multibyte string // on_pending_input() expects a multibyte string
const LineInfo *info_mb = el_line (editline); const LineInfo *info_mb = el_line (editline);
strv_append_owned (&ctx->pending_input, strv_append_owned (&ctx->pending_input,
xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer)); xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer));
@ -15170,7 +15184,15 @@ on_pending_input (struct app_context *ctx)
{ {
poller_idle_reset (&ctx->input_event); poller_idle_reset (&ctx->input_event);
for (size_t i = 0; i < ctx->pending_input.len; i++) for (size_t i = 0; i < ctx->pending_input.len; i++)
process_input (ctx, ctx->pending_input.vector[i]); {
char *input = iconv_xstrdup
(ctx->term_to_utf8, ctx->pending_input.vector[i], -1, NULL);
if (input)
process_input (ctx, ctx->current_buffer, input);
else
print_error ("character conversion failed for: %s", "user input");
free (input);
}
strv_reset (&ctx->pending_input); strv_reset (&ctx->pending_input);
} }
@ -15223,6 +15245,8 @@ client_resync (struct client *c)
{ {
relay_prepare_buffer_update (c->ctx, buffer); relay_prepare_buffer_update (c->ctx, buffer);
relay_send (c); relay_send (c);
relay_prepare_buffer_stats (c->ctx, buffer);
relay_send (c);
LIST_FOR_EACH (struct buffer_line, line, buffer->lines) LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
{ {
@ -15372,8 +15396,7 @@ client_process_message (struct client *c,
&m->data.buffer_complete); &m->data.buffer_complete);
break; break;
case RELAY_COMMAND_BUFFER_INPUT: case RELAY_COMMAND_BUFFER_INPUT:
(void) process_input_utf8 (c->ctx, process_input (c->ctx, buffer, m->data.buffer_input.text.str);
buffer, m->data.buffer_input.text.str, 0);
break; break;
case RELAY_COMMAND_BUFFER_ACTIVATE: case RELAY_COMMAND_BUFFER_ACTIVATE:
buffer_activate (c->ctx, buffer); buffer_activate (c->ctx, buffer);

View File

@ -89,15 +89,19 @@ body {
} }
.buffer { .buffer {
display: grid; display: grid;
grid-template-columns: max-content auto; grid-template-columns: max-content minmax(0, 1fr);
overflow-y: auto; overflow-y: auto;
} }
.log { .log {
padding: .1em .3em;
font-family: monospace; font-family: monospace;
white-space: pre-wrap;
overflow-y: auto; overflow-y: auto;
} }
.log, .content {
padding: .1em .3em;
/* Note: https://bugs.chromium.org/p/chromium/issues/detail?id=1261435 */
white-space: break-spaces;
overflow-wrap: break-word;
}
.leaked { .leaked {
opacity: 50%; opacity: 50%;
@ -136,10 +140,6 @@ body {
.mark.action { .mark.action {
color: darkred; color: darkred;
} }
.content {
padding: .1em .3em;
white-space: pre-wrap;
}
.content .b { .content .b {
font-weight: bold; font-weight: bold;
} }
@ -162,6 +162,7 @@ textarea {
margin: 0; margin: 0;
border: 2px inset #eee; border: 2px inset #eee;
flex-shrink: 0; flex-shrink: 0;
resize: vertical;
} }
textarea:focus { textarea:focus {
outline: none; outline: none;

View File

@ -110,8 +110,9 @@ class RelayRpc extends EventTarget {
if (typeof params !== 'object') if (typeof params !== 'object')
throw "Method parameters must be an object" throw "Method parameters must be an object"
// Left shifts in Javascript convert to a 32-bit signed number.
let seq = ++this.commandSeq let seq = ++this.commandSeq
if (seq >= 1 << 32) if ((seq << 0) != seq)
seq = this.commandSeq = 0 seq = this.commandSeq = 0
this.ws.send(JSON.stringify({commandSeq: seq, data: params})) this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
@ -131,13 +132,25 @@ class RelayRpc extends EventTarget {
let rpc = new RelayRpc(proxy) let rpc = new RelayRpc(proxy)
let buffers = new Map() let buffers = new Map()
let bufferLast = undefined
let bufferCurrent = undefined let bufferCurrent = undefined
let bufferLog = undefined let bufferLog = undefined
let bufferAutoscroll = true let bufferAutoscroll = true
function resetBufferStats(b) {
b.newMessages = 0
b.newUnimportantMessages = 0
b.highlighted = false
}
function bufferActivate(name) {
rpc.send({command: 'BufferActivate', bufferName: name})
}
let connecting = true let connecting = true
rpc.connect().then(result => { rpc.connect().then(result => {
buffers.clear() buffers.clear()
bufferLast = undefined
bufferCurrent = undefined bufferCurrent = undefined
bufferLog = undefined bufferLog = undefined
bufferAutoscroll = true bufferAutoscroll = true
@ -161,13 +174,20 @@ 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, { buffers.set(e.bufferName, (b = {lines: []}))
lines: [], resetBufferStats(b)
newMessages: e.newMessages,
newUnimportantMessages: e.newUnimportantMessages,
highlighted: e.highlighted,
})
} }
b.hideUnimportant = e.hideUnimportant
})
rpc.addEventListener('BufferStats', event => {
let e = event.detail, b = buffers.get(e.bufferName)
if (b === undefined)
return
b.newMessages = e.newMessages,
b.newUnimportantMessages = e.newUnimportantMessages
b.highlighted = e.highlighted
}) })
rpc.addEventListener('BufferRename', event => { rpc.addEventListener('BufferRename', event => {
@ -179,16 +199,16 @@ rpc.addEventListener('BufferRename', event => {
rpc.addEventListener('BufferRemove', event => { rpc.addEventListener('BufferRemove', event => {
let e = event.detail let e = event.detail
buffers.delete(e.bufferName) buffers.delete(e.bufferName)
if (e.bufferName === bufferLast)
bufferLast = undefined
}) })
rpc.addEventListener('BufferActivate', event => { rpc.addEventListener('BufferActivate', event => {
let old = buffers.get(bufferCurrent) let old = buffers.get(bufferCurrent)
if (old !== undefined) { if (old !== undefined)
old.newMessages = 0 resetBufferStats(old)
old.newUnimportantMessages = 0
old.highlighted = false
}
bufferLast = bufferCurrent
let e = event.detail, b = buffers.get(e.bufferName) let e = event.detail, b = buffers.get(e.bufferName)
bufferCurrent = e.bufferName bufferCurrent = e.bufferName
bufferLog = undefined bufferLog = undefined
@ -221,18 +241,21 @@ rpc.addEventListener('BufferLine', event => {
return return
} }
let visible = e.bufferName == bufferCurrent || e.leakToActive let visible = !document.hidden && bufferLog === undefined &&
(e.bufferName == bufferCurrent || e.leakToActive)
b.lines.push({...line}) b.lines.push({...line})
if (!visible || b.newMessages || b.newUnimportantMessages) { if (!(visible || e.leakToActive) ||
b.newMessages || b.newUnimportantMessages) {
if (line.isUnimportant) if (line.isUnimportant)
b.newUnimportantMessages++ b.newUnimportantMessages++
else else
b.newMessages++ b.newMessages++
} }
if (e.leakToActive) { if (e.leakToActive) {
let bc = buffers.get(bufferCurrent) let bc = buffers.get(bufferCurrent)
bc.lines.push({...line, leaked: true}) bc.lines.push({...line, leaked: true})
if (bc.newMessages || bc.newUnimportantMessages) { if (!visible || bc.newMessages || bc.newUnimportantMessages) {
if (line.isUnimportant) if (line.isUnimportant)
bc.newUnimportantMessages++ bc.newUnimportantMessages++
else else
@ -273,6 +296,12 @@ for (let i = 0; i < 24; i++) {
// ---- UI --------------------------------------------------------------------- // ---- UI ---------------------------------------------------------------------
let linkRE = [
/https?:\/\//,
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
/[^\[\](){}<>"'\s,.:]/,
].map(r => r.source).join('')
let Toolbar = { let Toolbar = {
toggleAutoscroll: () => { toggleAutoscroll: () => {
bufferAutoscroll = !bufferAutoscroll bufferAutoscroll = !bufferAutoscroll
@ -304,10 +333,6 @@ let Toolbar = {
} }
let BufferList = { let BufferList = {
activate: name => {
rpc.send({command: 'BufferActivate', bufferName: name})
},
view: vnode => { view: vnode => {
let items = Array.from(buffers, ([name, b]) => { let items = Array.from(buffers, ([name, b]) => {
let classes = [], displayName = name let classes = [], displayName = name
@ -322,7 +347,7 @@ let BufferList = {
} }
} }
return m('.item', { return m('.item', {
onclick: event => BufferList.activate(name), onclick: event => bufferActivate(name),
class: classes.join(' '), class: classes.join(' '),
}, displayName) }, displayName)
}) })
@ -345,17 +370,11 @@ let Content = {
}, },
linkify: (text, attrs) => { linkify: (text, attrs) => {
let re = new RegExp([ let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
/https?:\/\//,
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
/[^\[\](){}<>"'\s,.:]/,
].map(r => r.source).join(''), 'g')
let a = [], end = 0, match
while ((match = re.exec(text)) !== null) { while ((match = re.exec(text)) !== null) {
if (end < match.index) if (end < match.index)
a.push(m('span', attrs, text.substring(end, match.index))) a.push(m('span', attrs, text.substring(end, match.index)))
a.push(m('a', {href: match[0], ...attrs}, match[0])) a.push(m('a[target=_blank]', {href: match[0], ...attrs}, match[0]))
end = re.lastIndex end = re.lastIndex
} }
if (end < text.length) if (end < text.length)
@ -425,25 +444,38 @@ let Content = {
} }
let Buffer = { let Buffer = {
oncreate: vnode => { controller: new AbortController(),
if (vnode.dom !== undefined && bufferAutoscroll)
vnode.dom.scrollTop = vnode.dom.scrollHeight onbeforeremove: vnode => {
Buffer.controller.abort()
}, },
onupdate: vnode => { onupdate: vnode => {
Buffer.oncreate(vnode) if (bufferAutoscroll)
vnode.dom.scrollTop = vnode.dom.scrollHeight
},
oncreate: vnode => {
Buffer.onupdate(vnode)
window.addEventListener('resize', event => Buffer.onupdate(vnode),
{signal: Buffer.controller.signal})
}, },
view: vnode => { view: vnode => {
let lines = [] let lines = []
let b = buffers.get(bufferCurrent) let b = buffers.get(bufferCurrent)
if (b === undefined) if (b === undefined)
return return m('.buffer')
let lastDateMark = undefined let lastDateMark = undefined
let markBefore = b.lines.length let markBefore = b.lines.length
- b.newMessages - b.newUnimportantMessages - b.newMessages - b.newUnimportantMessages
b.lines.forEach((line, i) => { b.lines.forEach((line, i) => {
if (i == markBefore)
lines.push(m('.unread'))
if (line.isUnimportant && b.hideUnimportant)
return
let date = new Date(line.when) let date = new Date(line.when)
let dateMark = date.toLocaleDateString() let dateMark = date.toLocaleDateString()
if (dateMark !== lastDateMark) { if (dateMark !== lastDateMark) {
@ -451,14 +483,10 @@ let Buffer = {
lastDateMark = dateMark lastDateMark = dateMark
} }
if (i == markBefore)
lines.push(m('.unread'))
let attrs = {} let attrs = {}
if (line.leaked) if (line.leaked)
attrs.class = 'leaked' attrs.class = 'leaked'
// TODO: Make use of isUnimportant.
lines.push(m('.time', {...attrs}, date.toLocaleTimeString())) lines.push(m('.time', {...attrs}, date.toLocaleTimeString()))
lines.push(m(Content, {...attrs}, line)) lines.push(m(Content, {...attrs}, line))
}) })
@ -476,8 +504,21 @@ let Log = {
vnode.dom.scrollTop = vnode.dom.scrollHeight vnode.dom.scrollTop = vnode.dom.scrollHeight
}, },
linkify: text => {
let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
while ((match = re.exec(text)) !== null) {
if (end < match.index)
a.push(text.substring(end, match.index))
a.push(m('a[target=_blank]', {href: match[0]}, match[0]))
end = re.lastIndex
}
if (end < text.length)
a.push(text.substring(end))
return a
},
view: vnode => { view: vnode => {
return m(".log", {}, bufferLog) return m(".log", {}, Log.linkify(bufferLog))
}, },
} }
@ -542,11 +583,13 @@ let Input = {
let handled = false let handled = false
switch (event.keyCode) { switch (event.keyCode) {
case 9: case 9:
if (!event.shiftKey) 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.shiftKey) if (!event.ctrlKey && !event.metaKey && !event.altKey &&
!event.shiftKey)
handled = Input.submit(textarea) handled = Input.submit(textarea)
break break
} }
@ -570,6 +613,7 @@ let Main = {
return m('.xP', {}, [ return m('.xP', {}, [
m('.title', {}, [`xP (${state})`, m(Toolbar)]), m('.title', {}, [`xP (${state})`, m(Toolbar)]),
m('.middle', {}, [m(BufferList), m(BufferContainer)]), m('.middle', {}, [m(BufferList), m(BufferContainer)]),
// TODO: Indicate hideUnimportant.
m('.status', {}, bufferCurrent), m('.status', {}, bufferCurrent),
m(Input), m(Input),
]) ])
@ -577,3 +621,28 @@ let Main = {
} }
window.addEventListener('load', () => m.mount(document.body, Main)) window.addEventListener('load', () => m.mount(document.body, Main))
document.addEventListener('keydown', event => {
if (rpc.ws == undefined || event.ctrlKey || event.metaKey)
return
if (event.altKey && event.key == 'Tab') {
if (bufferLast !== undefined)
bufferActivate(bufferLast)
} else if (event.altKey && event.key == 'a') {
for (const [name, b] of buffers)
if (name !== bufferCurrent && b.newMessages) {
bufferActivate(name)
break
}
} else if (event.altKey && event.key == '!') {
for (const [name, b] of buffers)
if (name !== bufferCurrent && b.highlighted) {
bufferActivate(name)
break
}
} else
return
event.preventDefault()
})